VueJs: bind `v-on` on a custom component to replace an existing one - vue.js

In order to ease the styling of my page, I'd like to create a bunch of mini components like, and exploit how attributes are merged in VueJs. So for example, here is a minimal js file also hosted on this JSFiddle:
Vue.component('my-button', {
template: '<button style="font-size:20pt;"><slot></slot></button>'
})
var app = new Vue({
el: "#app",
data: {
message: "world",
},
methods: {
sayHello: function () {
alert("Hello");
}
}
})
and then in my html I just want to use <my-button> instead of button:
<div id="app">
Hello {{message}} <my-button #click="sayHello" style="color:red;">Style works, but not click</my-button> <button v-on:click="sayHello" style="color:red;">Both works</button>
</div>
Unfortunately, it seems that attributes are merged, but not listeners, so it means that I can't do v-on:click on my new button... Any way to make it possible?
Thanks!
-- EDIT --
I saw the proposition of Boussadjra Brahim of using .native, and it works, but then I found this link that explains why it's not a great practice and how to use v-on="$listeners" to map all listeners to a specific sub-button. However, I tried, to just change my template with:
template: `<button style="font-size:20pt;" v-on="$listeners"><slot></slot></button>`,
but I get an error:
Vue warn: Property or method "$listeners" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option."
Here is the JSFiddle.

Your fiddle didn't work because you were using an old version of Vue, $listeners was added in Vue 2.4.0.
Here's a demo:
Vue.component('my-button', {
template: '<button style="color: red" v-on="$listeners"><slot/></button>'
})
new Vue({
el: '#app',
methods: {
sayHello() {
alert('Hello')
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-button #click="sayHello">Custom Button</my-button>
<button #click="sayHello">Ordinary Button</button>
</div>

Related

How to display stub component when component is not found in vue

I am trying to catch situation, when component is not found, ie:
{
template: '<some-unknown-component></some-unknown-component>'
}
At that moment, Vue warns us with unknown custom element: <some-unknown-component>...
I would like to step in when some-unknown-component is not found and then use another component instead, like stub-component:
{
name: 'stub-component',
props: ['componentName'],
template: '<p>component ${componentName} does not exists, click here to create...</p>'
}
UPDATE: I am looking for solution without changing the template itself, so no v-if and component added.
Vue exposes a global error and warning handler. I managed to get a working solution by using the global warnHandler. I don't know if it is exactly what you are looking for, but it may be a good starting point. See the working snippet (I think it is quite self explanatory).
Vue.config.warnHandler = function (err, vm, info) {
if (err.includes("Unknown custom element:")) {
let componentName = err.match(/<.*>/g)[0].slice(1, -1)
vm.$options.components[componentName] = Vue.component('stub-component', {
props: ['componentName'],
template: `<p>component "${componentName}" does not exists, click here to create...</p>`,
});
vm.$forceUpdate()
} else {
console.warn(err)
}
};
new Vue({
el: '#app',
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<unknown-component></unknown-component>
</div>
Vue stores the details of all the registered components in the $options.component property of the Vue instance.
So, you can check for the component availability using this.$options.component and if the component is present then load the component otherwise load the other component.
In the below example, suppose you have two different components and you want to load them on the availability, then you can create a computed property on the basis of it, load the component as needed.
var CustomComponent = Vue.extend({ template: '<h2>A custom Component</h2>' });
var AnotherComponent = Vue.extend({ template: '<h2>Custom component does not exist.</h2>' });
new Vue({
el: "#app",
components: {
CustomComponent,
AnotherComponent
},
computed: {
componentAvailable () {
return this.$options.components.CustomComponent
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-if="componentAvailable">
<custom-component />
</div>
<div v-else>
<another-component />
</div>
</div>

Is it possible to DRY html in vuejs components without creating more components?

Lets say you have:
<template>
<div>
<!-- html for buttons -->
<!-- your form -->
<!-- html for buttons -->
</div>
</template>
<!-- rest of your component -->
Is it possible to DRY up the html for the html for buttons without using a separate component? It seems a lot of work to keep adding components just to save repeating 3-4 lines of html?
I don't know any Vue api that allows to do that properly, however there is a way.
There is v-html which would serve you for DRY html, but it would get rendered as plain HTML, so you cannot use Vue events from there -which I guess your buttons do-.
For instance:
//template
<div id="app">
<div v-html="dryContent"></div>
<p>{{content}}</p>
<div v-html="dryContent"></div>
<div v-html="computedString"></div>
</div>
//script
new Vue({
el: '#app',
data: {
content: 'some sentence',
dryContent: `<div>
<p>Hello world!</p>
</div>`
},
computed: {
computedString() {
return `<p>${this.content}</p>`
}
}
});
Will render the HTML properly. But you cannot setup vue event listeners in the rendered HTML.
You can still, however, setup native listeners:
dryContent: `<div>
<p onclick="console.log('foo')">Hello world!</p>
</div>`
And it will work.
And, well, there is this really obscure pattern which I totally don't suggest but that actually will fit your needs:
new Vue({
el: '#app',
data: {
content: 'some sentence',
dryContent: `<div>
<p onclick="modifyContent()">Hello world!</p>
</div>`
},
computed: {
computedString() {
return `<p>${this.content}</p>`
}
},
created() {
window.modifyContent = function() {
this.content = 'modified!!';
}.bind(this);
}
});
You export the component method to a window property, so you can call it from native code.
Don't know your use case, but I'm pretty sure I would just duplicate the HTML code or setup a new component instead of doing this.

Extend Vue.js v-on:click directive

I'm new to vuejs. I'm trying to create my first app. I would like to show a confirm message on every click on buttons.
Example:
<button class="btn btn-danger" v-on:click="reject(proposal)">reject</button>
My question is: Can I extend the v-on:click event to show the confirm everywhere? I would make my custom directive called v-confirm-click that first executes a confirm and then, if I click on "ok", executes the click event. Is it possible?
I would recommend a component instead. Directives in Vue are generally used for direct DOM manipulation. In most cases where you think you need a directive, a component is better.
Here is an example of a confirm button component.
Vue.component("confirm-button",{
props:["onConfirm", "confirmText"],
template:`<button #click="onClick"><slot></slot></button>`,
methods:{
onClick(){
if (confirm(this.confirmText))
this.onConfirm()
}
}
})
Which you could use like this:
<confirm-button :on-confirm="confirm" confirm-text="Are you sure?">
Confirm
</confirm-button>
Here is an example.
console.clear()
Vue.component("confirm-button", {
props: ["onConfirm", "confirmText"],
template: `<button #click="onClick"><slot></slot></button>`,
methods: {
onClick() {
if (confirm(this.confirmText))
this.onConfirm()
}
}
})
new Vue({
el: "#app",
methods: {
confirm() {
alert("Confirmed!")
}
}
})
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app">
<confirm-button :on-confirm="confirm" confirm-text="Are you sure?">
Confirm
</confirm-button>
</div>
I do not know of a way to extend a directive. It is easy enough to include a confirm call in the click handler. It won't convert every click to a confirmed click, but neither would writing a new directive; in either case, you have to update all your code to use the new form.
new Vue({
el: '#app',
methods: {
reject(p) {
alert("Rejected " + p);
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<button class="btn btn-danger" #click="confirm('some message') && reject('some arg')">reject</button>
</div>

Vue.js 2.0 Dynamic Props Docs

js 2.0 and I'm stock in dynamic props.
See Image attached
My HTML code like this:
<div id="app">
<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>
</div>
My Component code:
Vue.component('child', {
props: ['myMessage'],
template: '<p>{{ myMessage }}</p>',
})
var app = new Vue({
el: "#app",
});
I know that data should be a function but how I'm going to implement it. I get this error on the console.
Property or method "parentMsg" is not defined on the instance but referenced during render
I think message is clear. "parentMsg" is not defined on the instance. You have to define parentMsg at parent level. like following:
var app = new Vue({
data: {
"parentMsg": ""
}
el: "#app"
});
You can have a working fiddle here.

How to make Vue js directive working in an appended html element

I have a Vue directive added in an appended html element like v-on directive but it's not working on my end. Basically I want to know the equivalent of .on() in jquery.
"Vue.js compilation happens when you instantiate/mount the root instance. It doesn't detect new DOM being injected unless it is a result of a directive (e.g. v-repeat, v-partial will dynamically create new DOM fragments)."
https://github.com/vuejs/Discussion/issues/77
You have to compile your newly added element like this:
html:
<div id="app">
<button v-on="click: addNewElement()">Add Element</button>
<br />
</div>
JavaScript
new Vue({
el: '#app',
data: {
sampleElement: '<button v-on="click: test()">Test</button>'
},
methods:{
addNewElement: function(){
var element = $('#app').append(this.sampleElement);
/* compile the new content so that vue can read it */
this.$compile(element.get(0));
},
test: function(){
alert('Test');
}
}
});
See this working Fiddle on Firefox: http://jsfiddle.net/chrislandeza/syewmx4e/
Update
$compile has been removed on Vue 2.x
I've seen people suggesting Vue.compile or
var tmp = Vue.extend({
template: 'Content'
})
new tmp().$mount(' id or refs ')
But those 2 does not behave like the old $compile.
For Vue 2.x, the new solution is referenced here in the doc : https://v2.vuejs.org/v2/api/#vm-mount (see 'Example').
Custom example :
var MyComponent = Vue.extend({
template: '<div v-on:click="world">Hello!</div>',
methods : {
world : function() {
console.log('world')
}
}
})
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app"></div>
Works like a charm!
You need to register the html as a template of the component.