VueJS Loading template component via code? - vue.js

I am trying to make a template component that I can use later on in my project. However I'm having a bit of a hard time showing it on the element I want via code.
The code I have so far is as such.
<template>
<div>
<b-alert show dismissible variant="danger" v-show="elementVisible">
<i class="mdi mdi-block-helper mr-2"></i>{{ text }}
</b-alert>
</div>
</template>
<script>
export default {
name: "alertDanager",
props: {
text: null
},
data() {
return {
elementVisible: true
};
},
created() {
setTimeout(() => (this.elementVisible = false), 5000);
}
};
</script>
I am trying to call this on an action by this
I import it
import dangerAlert from "#/components/Alerts/danger";
Then on the function I want to call it on I do this
const error = new dangerAlert({ propsData: { text: "Error message" } });
error.$mount("#error");
However it just gives me an error saying
_components_Alerts_danger__WEBPACK_IMPORTED_MODULE_3__.default is not a constructor
So I'm not sure how to fix this or do what I need to do. I've tried googling but can't seem to find an answer.

The Component imported is not a constructor and it should extends a constructor and to use that you should use Vue.extend()
Vue.extend() is a class inheritance method. Its task is to create a sub-class of Vue and return the constructor.
so instead of this
const error = new dangerAlert({ propsData: { text: "Error message" } });
error.$mount("#error");
make it like this
const DangerAlertExtended= Vue.extend(dangerAlert);
const error = new DangerAlertExtended({ propsData: { text: "Error message" } });
error.$mount("#error");

Related

Making request to Spring Boot Admin server from custom view?

I'm trying to add a custom view with some administrative utilities to Spring Boot Admin. The idea is to implement these as endpoints in Springboot Admin and call these endpoints from my custom view, but I don't know how to make a call to the server itself.
When a custom view has parent: 'instances' it will get an axios client for connecting to the current instance, but since the view I'm building isn't tied to a specific instance it doesn't have this. I'm aware I can install axios as a dependency, but I'd like to avoid that if possible to reduce build times. Since SBA itself depends on axios it seems I shouldn't have to install it myself.
Based on this sample, this is what I have right now:
index.js
/* global SBA */
import example from './example';
import exampleEndpoint from './example-endpoint';
SBA.use({
install({viewRegistry}) {
viewRegistry.addView({
name: 'example',
path: '/example',
component: example,
label: 'Example',
order: 1000,
});
}
});
example.vue
<template>
<div>
<h1>Example View</h1>
<p>
<b>GET /example:</b> <span v-text="exampleResponse" />
</p>
</div>
</template>
<script>
export default {
props: {
applications: {
type: Array,
required: true
}
},
data: () => ({ exampleResponse: "No response" }),
async created() {
const response = await this.axios.get("example");
this.exampleResponse = response.response;
},
};
</script>
ExampleController.kt
#RestController
#RequestMapping("/example")
class ExampleController {
#GetMapping
fun helloWorld() = mapOf("response" to "Hello world!")
}
Console says that it can't read property get of undefined (i.e. this.axios is undefined). Text reads "GET /example: No response"
I'm not sure if this is the best way to do it, but it is a way.
I noticed that I do have access to the desired axios instance within the SBA.use { install(...) { } } block, and learned that this can be passed as a property down to the view.
index.js
/* global SBA */
import example from './example';
import exampleEndpoint from './example-endpoint';
SBA.use({
install({viewRegistry, axios}) {
viewRegistry.addView({
name: 'example',
path: '/example',
component: example,
label: 'Example',
order: 1000,
// this is where we pass it down with the props
// first part is the name, second is the value
props: { "axios": axios },
});
}
});
example.vue
<template>
<div>
<h1>Example View</h1>
<p>
<b>GET /example:</b> <span v-text="exampleResponse" />
</p>
</div>
</template>
<script>
export default {
props: {
applications: { type: Array, required: true },
// this is where we retrieve the prop. the name of the field should
// correspond to the name given above
axios: { type: Object, required: true },
},
data: () => ({
exampleResponse: "No response",
}),
async created() {
// Now we can use our axios instance! And it will be correctly
// configured for talking to Springboot Admin
this.axios.get("example")
.then(r => { this.exampleResponse = r.data.response; })
.catch(() => { this.exampleResponse = "Request failed!" });
},
};
</script>
Based on the code given, it looks like you don't have axios initialized to how you want to use it.
You're calling it via this.axios but it's not in your component i.e
data() {
return {
axios: require("axios") // usually this is imported at the top
}
}
or exposed globally i.e
Vue.prototype.axios = require("axios")
You can simply just import axios and reference it.
<script>
import axios from 'axios';
export default {
created() {
axios.get()
}
}
</script>

View not updating on data change

I'm currently working on a notification plugin in my project. I want every component to be able to trigger a notification like this:
this.$notification({
msg: 'error message'
});
To achieve that, I created a following plugin with a component:
Notifications.js
import Notifications from './Shared/Components/Notifications';
import Vue from 'vue';
export default {
install: (app, options) => {
app.component('notifications', Notifications)
Vue.prototype.$notification = function (options) {
Notifications.methods.show(options);
}
},
}
Notifications.vue
<template>
<div>
{{ message }}
</div>
</template>
<script>
export default {
name: "Notifications",
data() {
return {
message: '',
}
},
methods: {
show(options) {
this.message = options.msg;
}
}
}
</script>
But now, when another component triggers the plugin the view of the notifications.vue component does not change. But if I log the new value this.message it shows the new value. How do I achieve, that the new message "error message" is shown?
I already tried to watch the property, but it does not get triggered.

The object sent as prop is undefined while testing a Vue SFC with Jest

I want to test a Vue single file component which receives a prop as input. When I mock the prop, which is an object, I get an error that the object is undefined, The error comes from the HTML where the values of the object are used. If I make the prop to be a string for example (and I remove answer.value and :class="{'active': answer.selected}" from HTML), everything works fine.
Component:
<template>
<div class="answer-container" #click="setActiveAnswer()" :class="{'active': answer.selected}">
<div class="answer">
<p>{{answer.value}}</p>
</div>
</div>
</template>
<script>
export default {
name: 'Answer',
props: {
answer: Object,
},
methods: {
setActiveAnswer() {
this.$emit('selectedAnswer', this.answer);
}
}
}
</script>
Test file:
import { mount } from '#vue/test-utils'
import Answer from './../../src/components/Answer'
describe('Answer', () => {
it('should receive "answer" as prop', () => {
const answer = {
value: 'testAnswer',
selected: true
};
const wrapper = mount(Answer, {
propsData: {
answer: answer
}
});
expect(wrapper.props().answer.value).toBe('testAnswer');
})
})
The error I get is:
TypeError: Cannot read property 'selected' of undefined
Please advise what am I doing wrong. Thanks!
I managed to fix this by adding a v-if="answer" on <div class="answer-container" ..., which is quite strange (as this is not async data) since the code works fine when checking the application in the browser - the problem only appeared while unit testing the component. I suppose there is also a fix in a Jest/Unit testing way, something like declaring the prop after the component finished rendering/mounting...

set property and retrieve HTML as string from Vue component

In order to separate my code and make it cleaner, I would like to use a Vue component as an HTML template, pass some parameters to the template and get the resulting HTML back as a string.
I have made a simple example that almost works, but for some reason the returned HTML is not up to date. When I hit "click me" I do get an HTML-string from the "MyDetails"-component, but it shows the value passed from the previous time, I hit the "click me"-button, instead of showing the actual value.
Main.vue
<template>
<div>
<p>
<myDetails ref="myDetails"/>
</p>
<button #click="handleClick">click me</button>
<p>{{message}}</p>
</div>
</template>
<script>
import MyDetails from "/components/MyDetails.vue";
export default {
name: "hello",
components: {
MyDetails
},
methods: {
handleClick() {
this.$refs.myDetails.setMessage(new Date().getTime());
this.message = this.$refs.myDetails.$el.outerHTML;
}
},
data() {
return {
message: ""
};
}
};
</script>
MyDetails.vue
<template>
<div style="background-color:red">
<h1>MyDetails component</h1>
<p>{{message}}</p>
</div>
</template>
<script>
export default {
name: "hello",
data() {
return {
message: ""
};
},
methods: {
setMessage(value) {
this.message = value;
}
}
};
</script>
In the example above "MyDetails" is part of the template from the beginning. Is it possible to load it dynamically in the click-handler instead, so it doesn't show up, before I hit the "click me"-button?
Please see code here: https://codesandbox.io/s/vue-fullcalendar-example-50sv9?fontsize=14&hidenavigation=1&theme=dark
Updating the DOM takes time, you are immediately getting the myDetails outerHTML after you are changing its data, which doesn't give time for the change to propagate. Setting a slight delay as follows will give output as expected:
handleClick() {
this.$refs.myDetails.setMessage(new Date().getTime());
setTimeout(() => {
this.message = this.$refs.myDetails.$el.outerHTML;
}, 100)
}
For demo, see the sandbox here

Where should errors from REST API calls be handled when called from in vuex actions?

I have typical scenario where I call REST API in vuex actions to fetch some data and then I commit that to mutation.
I use async/await syntax and try/catch/finally blocks. My vuex module looks something like this:
const state = {
users: null,
isProcessing: false,
operationError: null
}
const mutations = {
setOperationError (state, value) {
state.operationError = value
},
setIsProcessing (state, value) {
state.isProcessing = value
if (value) {
state.operationError = ''
}
},
setUsers(state, value) {
state.users= value
}
}
const actions = {
async fetchUsers ({ commit }) {
try {
commit('setIsProcessing', true)
const response = await api.fetchUsers()
commit('setUsers', response.result)
} catch (err) {
commit('setUsers', null)
commit('setOperationError', err.message)
} finally {
commit('setIsProcessing', false)
}
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
Notice that I handle catch(err) { } in vuex action and don’t rethrow that error. I just save error message in the state and then bind it in vue component to show it if operationError is truthy. This way I want to keep vue component clean from error handling code, like try/catch.
I am wondering is this right pattern to use? Is there a better way to handle this common scenario? Should I rethrow error in vuex action and let it propagate to the component?
What I usually do, is have a wrapper around the data being posted, that handles the api requests and stores errors. This way your users object can have the errors recorded on itself and you can use them in the components if any of them are present.
For example:
import { fetchUsers } from '#\Common\api'
import Form from '#\Utils\Form'
const state = {
isProcessing: false,
form: new Form({
users: null
})
}
const mutations = {
setIsProcessing(state, value) {
state.isProcessing = value
},
updateForm(state, [field, value]) {
state.form[field] = value
}
}
const actions = {
async fetchUsers ({ state: { form }, commit }) {
let users = null
commit('setIsProcessing', true)
try {
users = await form.get(fetchUsers);
} catch (err) {
// - handle error
}
commit('updateForm', ['users', users])
commit('setIsProcessing', false)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
Then in the component you can use the errors object on the wrapper like so:
<template>
<div>
<div class="error" v-if="form.erros.has('users')">
{{ form.errors.get('users') }}
</div>
<ul v-if="users">
<li v-for="user in users" :key="user.id">{{ user.username }}</li>
</ul>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState('module' ['form']),
users () {
return this.form.users
}
}
</script>
This is just my personal approach that I find very handy and it served me well up to now. Don't know if there are any standard patterns or if there is an explicit "correct way" to do this.
I like the wrapper approach, because then your errors become automatically reactive when a response from api returns an error.
You can re-use it outside vuex or even take it further and inject the errors into pre-defined error boundaries which act as wrapper components and use the provide/inject methods to propagate error data down the component tree and display them where ever you need them to show up.
Here's an example of error boundary component:
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
module: {
type: String,
required: true,
validator: function (value) {
return ['module1', 'module2'].indexOf(value) !== -1
}
},
form: {
type: String,
default: 'form'
}
},
provide () {
return {
errors: this.$store.state[this.module][this.form].errors
}
}
}
</script>
Wrap some part of the application that should receive the errors:
<template>
<div id="app">
<error-boundary :module="module1">
<router-view/>
</error-boundary>
</div>
</template>
Then you can use the errors from the users wrapper in child components like so:
If you have a global error like no response from api and want to display it in the i.e.: sidebar
<template>
<div id="sidebar">
<div v-if="errors.has('global')" class="error">
{{ errors.get('global').first() }}
</div>
...
</div>
</template>
<script>
export default {
inject: [
'errors'
],
...
}
</script>
And the same error object re-used somewhere inside a widget for an error on the users object validation:
<template>
<div id="user-list">
<div v-if="errors.has('users')" class="error">
{{ errors.get('users').first() }}
</div>
...
</div>
</template>
<script>
export default {
inject: [
'errors'
],
...
}
</script>
Jeffrey Way did a series on Vue2 a while ago and he proposed something similar. Here's a suggestion on the Form and Error objects that you can build upon: https://github.com/laracasts/Vue-Forms/blob/master/public/js/app.js