I have a rather simple component. Basically it is a dropdown select and I need to run some custom code that is in an external javascript function. However, this prop is not required on every instance of the component. So sometimes there may be nothing in the prop. Other times it might do one thing, others might do something else.
<template id="drop-list-template">
<select class="form-control"
v-model="value"
v-bind:class="{ required: isRequired, invalid: !isValid }"
v-on:blur="validate"
v-on:change="changed"> <-- This is the Prop I want to use
<option v-if="showEmptyOption" value="">{{ emptyOption }}</option>
<option v-for="i in items"
v-bind:value="i.value"
v-bind:selected="i.checked === value"
v-bind:disabled="i.enabled === false">
{{ i.text }}
</option>
</select>
</template>
So in the on-change event, it will call the changed method. That was not working. I then added a special prop to the code file:
Vue.component("drop-list", {
template: "#drop-list-template",
props: {
dataset: { type: Array, required: true },
isRequired: { type: Boolean, required: false, default: false },
emptyOption: { type: String, required: false, default: "*Select an Option *" },
showEmptyOption: { type: Boolean, required: false, default: true },
special: { required: false }
},
data: function () {
return {
items: this.dataset,
isValid: true,
value: ""
}
},
methods: {
validate: function (event) {
var Result = true;
if ((this.isRequired === true) && (this.value === ""))
Result = false;
this.isValid = Result;
return Result;
},
changed: function (event) {
if (this.special) {
AbnormalitiesAndImpressions(); <-- Obviously this works
alert("After");
this.special(); <-- Would want this to run AbnormalitiesAndImpressions
}
}
}
});
And implement it via:
<drop-list ref="lstAbnormalities"
v-bind:dataset="Abnormalities"
v-bind:is-required="true"
special="AbnormalitiesAndImpressions">
</drop-list>
Where AbnormalitiesAndImpressions is just dumb right now:
function AbnormalitiesAndImpressions(lstAbs, lstImps) {
alert("Got to here");
}
When I run it, the "Got to here" alert pops up and so does the "After" alert. It then fails because this.special(); is not a function.
Bottom line is I am trying to let the user (myself in this case) create as many of these lists as needed. What will happen on some of them is they tweak what is available in other controls. So a sort of validation is going on. I just want this to be customizable per each use of the component.
I would even be fine with an anonymous function like the following:
<drop-list ref="lstAbnormalities"
v-bind:dataset="Abnormalities"
v-bind:is-required="true"
special="function () { AbnormalitiesAndImpressions(); }">
</drop-list>
Update
I have updated my component slightly:
<template id="drop-list-template">
<select class="form-control"
v-model="value"
v-bind:class="{ required: isRequired, invalid: !isValid }"
v-on:blur="validate"
v-on:change="change">
<option v-if="showEmptyOption" value="">{{ emptyOption }}</option>
<option v-for="i in items"
v-bind:value="i.value"
v-bind:selected="i.checked === value"
v-bind:disabled="i.enabled === false">
{{ i.text }}
</option>
</select>
</template>
And the corresponding javascript:
Vue.component("drop-list", {
template: "#drop-list-template",
props: {
dataset: { type: Array, required: true },
isRequired: { type: Boolean, required: false, default: false },
emptyOption: { type: String, required: false, default: "*Select an Option *" },
showEmptyOption: { type: Boolean, required: false, default: true },
special: { type: Function, required: false }
},
data: function () {
return {
items: this.dataset,
isValid: true,
value: ""
}
},
methods: {
change: function (event) {
if (this.special)
this.special();
}
}
});
And the implementation:
<drop-list ref="lstAbnormalities"
v-bind:dataset="Abnormalities"
v-bind:is-required="true"
:special="AbnormalitiesAndImpressions">
</drop-list>
And here is the page's Vue code:
var vm = new Vue({
el: "#app",
data: {
Result: {},
Defaults: {},
Errors: [],
Abnormalities: [],
Impressions: []
},
methods: {
AbnormalitiesAndImpressions: function () {
alert("Should get overridden");
}
}
});
vm.AbnormalitiesAndImpressions = function (lstAbs, lstImps) {
alert("Got to here: " + lstAbs + "\n" + lstImps);
}
I found that if I did not add the methods short version of AbnormalitiesAndImpressions that it would give me a Vue warning that the property did not exist. However, the version of AbnormalitiesAndImpressions at the bottom of that file actually runs. I like this as each implementation could change and they should be on the page and not on the component.
When I change the dropdown item, I do get the Got to here message. And of course it has two undefined as the lstAbs and lspImps were not passed in.
New Question
Is it possible then to pass values to my props function? In this case, they can be strings. But if I do the code below...
<drop-list ref="lstAbnormalities"
v-bind:dataset="Abnormalities"
v-bind:is-required="true"
:special="AbnormalitiesAndImpressions('test')">
</drop-list>
When the page loads, the alert is popped right away and Does have the test parameter. And when I actually change the select the alert does not fire at all.
Ok, so let's do it step by step.
Correct method declaration
methods: {
change: function (event) {
if (this.special)
this.special();
}
}
This method declaration isn't good, it's changing the this context, always declare methods with arrow functions or with the shorthand syntax. Anonymous functions declared like this: function () { //... } creates a new this context, and beacuse of that this.special is always undefined. So change it to:
methods: {
change (event) {
if (this.special)
this.special();
}
}
Do it at all methods, it'll avoid a lot of headache. To another anonymous functions, always use arrow functions.
Method passed as prop
About your new question, let me explain what's happening when you set the special prop as AbnormalitiesAndImpressions with vanilla Js to clarify your mind.
Think about a method foo, just like this below:
function foo (string) {
return string;
}
Above we can see the method declaration, in Js is possible to assign a function to a variable, so, if a create a variable a it can be equals to foo, just like it:
let a = foo;
As you can see, I'm passing the function foo to the var a, not the return of the function foo, it's what you do when you set the special property as AbnormalitiesAndImpressions, because of that you can't do this: :special="AbnormalitiesAndImpressions('test')", but, if we look back to my example, one thing we can do, that is:
a('bar');
And it'll return 'bar', so, applying it to Vue, at your component drop-list, where you call the function as this.special you can pass params to the function, did you get it?
Related
I want to use v-for and is to render the components I need. So I create the component Cube.vue like this:
<script>
export default {
name: 'cube',
render: function(createElement) {
return createElement("div",{
props: {
style: {
type: Object
},
dragstartHandler: {
type: Function
},
classNames: {
type: Array|String|Object
}
},
style: this.style,
attrs: {
draggable: "true",
},
on: {
dragstart: this.dragstartHandler
},
'class': this.classNames
},[
createElement("div",{
class: ["resizer", "top-left"]
}),
createElement("div",{
class: ["resizer", "top-right"]
}),
createElement("div",{
class: ["resizer", "bottom-left"]
}),
createElement("div",{
class: ["resizer", "bottom-right"]
}),
])
}
}
</script>
And then, I use it like this
<component
v-for="component in components"
:class="component.classes"
:key="component.refValue"
:is="component.type"
:style="component.style"
:dragstartHandler="component.dragstartHandler"
:ref="component.refValue"
>
</component>
Everything as I expected, except the dragstartHandler. It throws an error
[Vue warn]: Invalid handler for event "dragstart": got undefined
I try to console.log() the components. The result is :
[{…}, __ob__: Observer]
0:
classes: Array(2)
dragstartHandler: ƒ ()
refValue: "cube-0"
style: Object
type: "cube"
It really is a function. But I don't know why it go to undefined in the render. I have checked I didn't spell wrong.
I just want pass the function to the component to handle the drag event. So how does it happened and what should I do to resolve it.
The dragHandler function is these:
dragstartCopyHandler(event) {
event.dataTransfer.setData("elementId", event.target.id);
event.dataTransfer.setData("componentOffsetX", event.offsetX);
event.dataTransfer.setData("componnetOffsetY", event.offsetY);
event.dataTransfer.setData("dropEffect", "copy");
event.dataTransfer.dropEffect = "copy";
},
dragstartMoveHandler(event) {
console.log("move start")
event.dataTransfer.setData("elementId", event.target.id);
event.dataTransfer.setData("componentOffsetX", event.offsetX);
event.dataTransfer.setData("componnetOffsetY", event.offsetY);
event.dataTransfer.setData("dropEffect", "move");
event.dataTransfer.dropEffect = "move";
},
And I pass the dragstartMoveHandler to the component.
this.components.push({
refValue: `${elementId}-${this.count}`,
type: elementId,
style: style,
dragstartHandler: this.dragstartMoveHandler,
classes: ["component", elementId]
});
I write the pages use js to control Dom before. And today I want to rewrite it with vue. So here might something wrong with the function, but the problem now is the function passed is undefined.
The cube component needs to define a prop for dragstartHandler so that it receives it from the parent for passing it on to the div. The same is true of the style and class bindings, which are also not working as you expect, but you don't see an error there because those are built-in bindings which get transferred to the root element.
Cube.vue
render() {
...
},
props: {
dragstartHandler: {
type: Function,
},
}
I'm using a combobox with input data validation (Vuelidate):
<template>
<v-combobox
clearable
v-model="surname"
:items="commonSurnames"
label="Surname"
placeholder="Type in the surname"
class="pt-5 pb-5"
:error-messages="surnameErrors"
#input="$v.surname.$touch()"
#blur="$v.surname.$touch()">
</v-combobox>
</template>
<script>
import { validationMixin } from 'vuelidate'
import { required, maxLength } from 'vuelidate/lib/validators'
export default {
mixins: [validationMixin],
validations: {
surname: {
required,
maxLength: maxLength(30),
validSurname(surname) {
return (
/^[a-zA-Z]-?*.+$/.test(surname)
)
}
},
name: 'Surnames',
data() {
return {
surname: '',
[...]
},
methods: {
[...]
},
computed: {
surnameErrors() {
const errors = []
if (!this.$v.surname.$dirty) return errors
!this.$v.surname.validSurname && errors.push('Format must be like: Smith or Smith-Wesson')
!this.$v.surname.maxLength && errors.push('Surname must be at most 30 characters long.')
!this.$v.surname.required && errors.push('Surname is required.')
return errors
}
}
</script>
Versions of components:
"dependencies": {
"#vue/compiler-sfc": "^3.0.0",
"core-js": "^3.6.5",
"vue": "^2.6.11",
"vuelidate": "^0.7.5",
"vuetify": "^2.2.11"
},
I suppose I did everything as in the Vuetify Documentation, but my form gets validated a bit differently than what is there, in the docs: I can exceed the limit of 30 characters without being notified about it while typing. I only know it when the input loses focus. The same situation with RegEx validation: any value is accepted without error notification. If the value is not valid, I get notified when leaving the input field.
Did I miss something when copying the example from the docs, or the #input listener works incorrectly? Or is it that v-combobox can't be validated this way?
v-combobox emits change or input events only when one of its valid values from the dropdown are selected. The text entered in the combobox is actually search input, used to lookup a valid value; and that entry is bound to v-combobox's search-input prop.
The v-combobox API lists update:search-input as an event that is fired when the search input changes, so you could use that to bind the validation handler:
<!-- BEFORE: -->
<v-combobox #input="$v.surname.$touch()">
<!-- AFTER: -->
<v-combobox #update:search-input="$v.surname.$touch()">
I used to have a component wrapper around v-combobox, v-autocomplete and v-select (yes, single multi-purpose wrapper). And I would recommend using the code below in a wrapper to avoid copy pasting the fix/workaround.
In general workaround required lookup into Vuetify sources. This led to manipulating errorBucket and valid in #update:search-input listener.
Additionally you might want to emit 'input'. It requires one small tweak to suppress propagation into v-combobox value, since it breaks autocomplete behavior otherwise. Hence if (this.search) { return; }
Full example: Codepen
Vue.component(
'my-combobox', {
template: `
<v-combobox
ref="combobox"
outlined dense
v-model="selection"
v-bind="{ ...$attrs, items, rules: validationRules }"
v-on="$listeners"
#update:search-input="onSearch"
#focus="touched = true"></v-combobox>
`,
props: {
value: String,
items: Array,
required: Boolean,
rules: Array,
},
data() {
return {
selection: null,
search: null,
touched: false,
};
},
computed: {
validationRules() {
return [
...(this.rules || []),
(v) => !this.required || (v?.length ?? 0) > 0 || 'Value is required',
];
},
},
methods: {
onSearch(v) {
if (!this.touched) return;
this.search = v;
const $self = this.$refs.combobox;
$self.errorBucket = this.validationRules.filter(f => f(v) !== true);
$self.valid = $self.errorBucket.length === 0;
this.$emit('input', v);
},
},
watch: {
selection: {
handler(v) {
this.$emit('input', v);
},
},
value: {
immediate: true,
handler(v) {
if (this.search) { return; }
this.selection = v;
},
},
},
});
I am struggeling with a proper solution which requires an advanced parent-child communication in vuejs. There can be many different parent components which has a logic how to save data. From the other side there will be only one child component which has a list of elements and a form to create new elements but it doesn't know how to save the data.
The question is: Is there any other way (better approach) to have the same functionality but to get rid of this.$refs.child links. For example I am wondering if I can just pass a function (SaveParent1(...) or SaveParent2(...)) to the child component. But the problem is the function contains some parent's variables which won't be available in child context and those variables could be changed during the runtime.
Just few clarifications:
The methods SaveParent1 and SaveParent2 in real life return
Promise (axios).
The child-component is like a CRUD which is used
everywhere else.
At the moment the communication looks like that: CHILD -event-> PARENT -ref-> CHILD.
Bellow is the example:
<div id="app">
<h2>😀Advanced Parent-Child Communication:</h2>
<parent-component1 param1="ABC"></parent-component1>
<parent-component2 param2="XYZ"></parent-component2>
</div>
Vue.component('parent-component1', {
props: { param1: { type: String, required: true } },
methods: {
onChildSubmit(p) {
// Here will be some logic to save the param. Many different parents might have different logic and all of them use the same child component. So child-component contains list, form and validation message but does not know how to save the param to the database.
var error = SaveParent1({ form: { p: p, param1: this.param1 } });
if (error)
this.$refs.child.paramFailed(error);
else
this.$refs.child.paramAdded(p);
}
},
template: `<div class="parent"><p>Here is parent ONE:</p><child-component ref="child" #submit="onChildSubmit"></child-component></div>`
});
Vue.component('parent-component2', {
props: { param2: { type: String, required: true } },
methods: {
onChildSubmit(p) {
// Here is a different logic to save the param. In prictice it is gonna be different requests to the server.
var error = SaveParent2({ form: { p: p, param2: this.param2 } });
if (error)
this.$refs.child.paramFailed(error);
else
this.$refs.child.paramAdded(p);
}
},
template: `<div class="parent"><p>Here is parent TWO:</p><child-component ref="child" #submit="onChildSubmit"></child-component></div>`
});
Vue.component('child-component', {
data() {
return {
currentParam: "",
allParams: [],
errorMessage: ""
}
},
methods: {
submit() {
this.errorMessage = "";
this.$emit('submit', this.currentParam);
},
paramAdded(p) {
this.currentParam = "";
this.allParams.push(p);
},
paramFailed(msg) {
this.errorMessage = msg;
}
},
template: `<div><ol><li v-for="p in allParams">{{p}}</li></ol><label>Add Param: <input v-model="currentParam"></label><button #click="submit" :disabled="!currentParam">Submit</button><p class="error">{{errorMessage}}</p></div>`
});
function SaveParent1(data) {
// Axios API to save data. Bellow is a simulation.
if (Math.random() > 0.5)
return null;
else
return 'Parent1: You are not lucky today';
}
function SaveParent2(data) {
// Axios API to save data. Bellow is a simulation.
if (Math.random() > 0.5)
return null;
else
return 'Parent2: You are not lucky today';
}
new Vue({
el: "#app"
});
There is also a live demo available: https://jsfiddle.net/FairKing/novdmcxp/
Architecturally I recommend having a service that is completely abstract from the component hierarchy and that you can inject and use in each of the components. With this kind of component hierarchy and architecture it is easy to run into these issues. It is important to abstract as much functionality and business logic from the components as possible. I think of components in these modern frameworks just merely as HTML templates on steroids, which should at most act as controllers, keeping them as dumb and as thin as possible so that you don't run into these situations. I do not know vue.js so I cannot give you the technical solution but hope this indication helps
I think I have found a solution. So no two ways communication. I can just pass a method and the child will do everything without communicating with parent. I am happy with that I am marking it as an answer. Thanks everyone for your help.
Let me please know what do you think guys.
Bellow is my solution:
<div id="app">
<h2>😀Advanced Parent-Child Communication:</h2>
<parent-component1 param1="ABC"></parent-component1>
<parent-component2 param2="XYZ"></parent-component2>
</div>
Vue.component('parent-component1', {
props: { param1: { type: String, required: true } },
computed: {
saveFunc() {
return function(p) { SaveParent1({ form: { p: p, param1: this.param1 } }); }.bind(this);
}
},
template: `<div class="parent"><p>Here is parent ONE:</p><child-component :saveFunc="saveFunc"></child-component></div>`
});
Vue.component('parent-component2', {
props: { param2: { type: String, required: true } },
computed: {
saveFunc() {
return function(p) { SaveParent2({ form: { p: p, param2: this.param2 } }); }.bind(this);
}
},
template: `<div class="parent"><p>Here is parent TWO:</p><child-component :saveFunc="saveFunc"></child-component></div>`
});
Vue.component('child-component', {
props: {
saveFunc: { type: Function, required: true }, // This is gonna be a Promise in real life.
},
data() {
return {
currentParam: "",
allParams: [],
errorMessage: ""
}
},
methods: {
submit() {
this.errorMessage = "";
var error = this.saveFunc(this.currentParam);
if (error)
this.paramFailed(error);
else
this.paramAdded(this.currentParam);
},
paramAdded(p) {
this.currentParam = "";
this.allParams.push(p);
},
paramFailed(msg) {
this.errorMessage = msg;
}
},
template: `<div><ol><li v-for="p in allParams">{{p}}</li></ol><label>Add Param: <input v-model="currentParam"></label><button #click="submit" :disabled="!currentParam">Submit</button><p class="error">{{errorMessage}}</p></div>`
});
function SaveParent1(data) {
console.log(data);
// Axios API to save data
if (Math.random() > 0.5)
return null;
else
return 'Parent1: You are not lucky today';
}
function SaveParent2(data) {
console.log(data);
// Axios API to save data
if (Math.random() > 0.5)
return null;
else
return 'Parent2: You are not lucky today';
}
new Vue({
el: "#app"
});
The demo link: https://jsfiddle.net/FairKing/novdmcxp/126/
I want to call the method from another component in let's say parent component... So, let's say I have notie.vue and there's my method named 'flash' and I want to call that after API request when I get success.
So let's say this is the case: after get API I want to call something like this with parameters ---> notie.success('notification type', 'notification message')
Here's the code:
Props are defined like this
props: {
type: {
default: 'info',
required: false
},
content: {
default: 'This is just demo text',
required: false
},
visible: {
type: Boolean,
default: false
}
}
Here's data
data: function () {
return {
show: true
}
}
Here's method
methods: {
flash: function () {
this.show = true;
}
}
Here's HTML code
<div class="notie" v-bind:class="[{active: show}, type]" v-on:click="close()">
<div class="notie__icon" :class="type"></div>
<div class="notie__content" v-text="content"></div>
</div>
Here for example i want to call my notie component
submit: function () {
this.$http.post('/api/books/add', {
data: this.data,
}).then(function (response) {
// I want to use notie component right here to notice to users.
}, function (response) {
});
}
So my question is how can I call flash() after API request on success or error?
I have an alert component like in this video: https://laracasts.com/series/learning-vue-step-by-step/episodes/21 And I have another component (Book). When a book was created how can I call Alert component in the success callback function like this:
<alert>A book was created successfully !!!</alert>
I am a newbie in using vue.js. Thank you for your help.
Updated: This is my code
submit: function () {
this.$http.post('/api/books/add', {
data: this.data,
}).then(function (response) {
// I want to use Alert component right here to notice to users.
}, function (response) {
});
}
Update 2:
Alert Component
<template>
<div class="Alert Alert--{{ type | capitalize }}"
v-show="show"
transition="fade"
>
<slot></slot>
<span class="Alert__close"
v-show="important"
#click="show = false"
>
x
</span>
</div>
</template>
<script>
export default {
props: {
type: { default: 'info' },
timeout: { default: 3000 },
important: {
type: Boolean,
default: false
}
},
data() {
return {show: true};
},
ready() {
if (!this.important)
{
setTimeout(
() => this.show = false,
this.timeout
)
}
}
}
</script>
<style>
.Alert {
padding: 10px;
position: relative;
}
.Alert__close {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
}
.Alert--Info {
background: #e3e3e3;
}
.fade-transition {
transition: opacity 1s ease;
}
.fade-leave {
opacity: 0;
}
</style>
And in Book.vue I want to do like this:
// resources/assets/js/components/Book.vue
<template>
.....
<alert>A book was created successfully !!!</alert>
//Create book form
....
</template>
<script>
export default {
methods: {
submit: function () {
this.$http.post('/api/books/add', {
data: this.data,
}).then(function (response) {
this.$refs.alert
}, function (response) {
});
}
</script>
this JSfiddle does what you're looking for: https://jsfiddle.net/mikeemisme/s0f5xjxu/
I used a button press rather than a server response to trigger the alert, and changed a few method names, but principle is the same.
The alert component is nested inside the button component. Button passes a showalert prop to the alert component with the sync modifier set.
<alert :showalert.sync="showalert" type="default" :important="true">A book was saved successfully</alert>
Press the button, showalert is set to 'true', 'true' passed to alert as prop, alert displays as v-show condition is now true,
data() {
//by default we're not showing alert.
//will pass to alert as a prop when button pressed
//or when response from server in your case
return {
showalert: false
};
},
a watch on the showalert prop in alert component sees a change and triggers a method that sets showalert back to 'false' after whatever many seconds set in timeout property.
//this method is triggered by 'watch', below
//when 'showalert' value changes it sets the timeout
methods: {
triggerTimeout: function() {
//don't run when detect change to false
if (this.showalert === true) {
setTimeout(
() => this.showalert = false,
this.timeout
)
}
},
},
watch: {
// detect showalert being set to true and run method
'showalert': 'triggerTimeout',
}
Because this prop is synched back to parent, button state updated too.
It works but using watch etc. feels overblown. Vue may have a better way to handle this. I'm new to Vue so somebody with more knowledge might chime in.
Add a data property
alertShow: false
Next, in the callback:
this.alertshow = true;
When you want to remove it, set it to false.
In the component add a directive:
v-show="alertshow"
Update:
Add a components attribute to block component.
components: {Alert},
Next outside of the component, import the Alert component file:
import Alert from './directory/Alert.vue'
The above is if you are using vueify. Otherwise, add a component using
Vue.component
Check out the docs.
Update 2:
Your code, with the changes:
<script>
import Alert from './directory/alert.vue';
export default {
components: {
Alert
},
methods: {
submit: function () {
this.$http.post('/api/books/add', {
data: this.data,
}).then(function (response) {
this.$refs.alert
}, function (response) {
});
}