I'm trying to use Chart.js with Vue.js and this is what I got it is compiling but I don't see anything displayed on the GUI.
This is my file DonutChart.vue:
<template>
// NOT SURE IF SOMETHING SHOULD GO HERE
</template>
<script>
import {Bar} from 'vue-chartjs'
// import the component - chart you need
export default Bar.extend({
mounted () {
// Overwriting base render method with actual data.
this.renderChart({
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
datasets: [
{
label: 'News reports',
backgroundColor: '#3c8dbc',
data: [12, 20, 12, 18, 10, 6, 9, 32, 29, 19, 12, 11]
}
]
},)
}
});
</script>
This is the parent component, ´Usage.vue´:
<template>
<h1>USAGE</h1>
<st-donut-chart></st-donut-chart>
</template>
<script>
import Vue from 'vue';
import Filter from './shared/filter/Filter';
import DonutChart from './DonutChart'
export default new Vue({
name: 'st-usage',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
components: {
'st-filter': Filter,
'st-donut-chart': DonutChart,
}
});
</script>
DonutChart.vue and Usage.vue are on the same directory:
vue-chartjs author here.
Well it's a bit confusing for beginners. However vue-chartjs is utilizing Vue.extend().
That's why you're have to extent the imported component.
Step 1
Create your own component and extend the base chart. This way you have more control over everything.
Your DonutChart.vue was nearly right. But you have to remove the <template> from your component. Because of the Vue.extend() you're extending the base component. So you have access to the props, methods etc. defined there. However there is no way of extending templates. So if you let the template tag in your component, it will overwrite the template defined in the base chart, that you're extending. Thats why you can't see anything ;)
YourChart.vue:
<script>
// Import the base chart
import {Bar} from 'vue-chartjs'
// Extend it
export default Bar.extend({
props: ['chartdata', 'options'],
mounted () {
// Overwriting base render method with actual data and options
this.renderChart(this.chartdata, this.options)
}
})
</script>
Now you have the Chart Component. You can put more login in there, of define some styles or options.
Step 2
Import it and feed it the data.
Like you did :)
Update
With version 3 of vue-chartjs the creation has changed. Now its more vue-like.
<script>
// Import the base chart
import {Bar} from 'vue-chartjs'
// Extend it
export default {
extends: Bar,
props: ['chartdata', 'options'],
mounted () {
// Overwriting base render method with actual data and options
this.renderChart(this.chartdata, this.options)
}
}
</script>
Or you can use mixins
<script>
// Import the base chart
import {Bar} from 'vue-chartjs'
// Extend it
export default {
mixins: [Bar],
props: ['chartdata', 'options'],
mounted () {
// Overwriting base render method with actual data and options
this.renderChart(this.chartdata, this.options)
}
}
</script>
So now I got it working:
import DonutChart from './DonutChart'
export default ({ //<= Notice change here
name: 'st-usage',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
components: {
'st-filter': Filter,
'line-example':LineExample,
'st-donut-chart':DonutChart,
}
});
Related
I'm using Vue 3 with ag-grid and want to setup a new ColDef like so
const colDef: ColDef = {
field: 'objectKey',
headerComponent: ColumnHeader, // my custom component
headerComponentParams: {
foo: 'bar'
},
}
My ColumnHeader component defines its props for now
<script setup lang="ts">
const props = defineProps<{
foo: string;
}>();
</script>
Running the app gives me the following error
[Vue warn]: Missing required prop: "foo" ...
This is because the whole props are undefined.
For reproduction purposes
Plunker snippet https://plnkr.co/edit/OoOD0I8W5NgYX45u which is based on https://www.ag-grid.com/vue-data-grid/component-header/#example-custom-header-component
You will get the error
Missing required prop: "name" ...
Based on https://github.com/ag-grid/ag-grid-vue-example/issues/14 it should work as expected I think. Does someone know what's wrong or missing?
your document clearly states 1. how to use headerComponent in columnDefs and 2. how parameters are passed down to custom header components.
pass a component name as string, just like mounting an external component with <component />. It receives both component itself and mounted component's name in string. My guess is that AgGridVue also uses <component /> internally.
// main.js
data: function () {
return {
rowData: [
{
foo: 'bar',
},
],
columnDefs: [
{
field: 'foo',
headerComponent: 'CustomHeader',
headerComponentParams: {
name: 'test',
},
},
],
};
},
When a Vue component is instantiated the grid will make the grid APIs, a number of utility methods as well as the cell and row values available to you via this.params - the interface for what is provided is documented below.
modify ColumnHeader to use this.params instead of props.
// customHeaderVue.js
export default {
template: `
<div>
*{{ name }}*
</div>
`,
computed: {
name() {
return this.params.name;
},
},
};
working demo: https://plnkr.co/edit/L7X6q3Mr7K0pewO8
edit: ag-grid's IHeaderParams does not support generics. to extend given type without generics, please use these methods.
import type { IHeaderParams } from "ag-grid-community";
// fig 1
// combine with type intersection
type CustomHeaderParams = IHeaderParams & { name: string };
// fig2
// combine with interface extension
interface CustomHeaderParams extends IHeaderParams {
name: string;
}
here are typed examples of CustomHeader.vue
// fig 1. Vue3 composition API, with <script setup>
<script lang="ts" setup>
import { defineProps, onMounted, ref } from "vue";
import type { IHeaderParams } from "ag-grid-community";
type CustomHeaderParams = IHeaderParams & { name: string };
const props = defineProps<{ params: CustomHeaderParams }>();
const name = ref(props.params.name);
</script>
<template>
<div>*{{ name }}*</div>
</template>
// ------
// 2. Vue3 options API, without <script setup>
<script lang="ts">
import { defineComponent, type PropType } from "vue";
import type { IHeaderParams } from "ag-grid-community";
type CustomHeaderParams = IHeaderParams & { name: string };
export default defineComponent({
props: {
params: {
type: Object as PropType<CustomHeaderParams>,
},
},
setup(props) {
return { name: props.params?.name ?? "" };
},
});
</script>
<template>
<div>*{{ name }}*</div>
</template>
note: I've suggested using the component's name in columnDefs.headerComponent because the official document says so, and seems fine in the working demo; but it actually depends on the Vue API. I assume it has something to do with Vue internal variable scope.
Vue Options API(Class based component): put component name string.
Vue Compositions API(Functional component): put the component variable itself.
I'd like to conditionnaly import a component in the vue router. Here is what I have for the moment:
children: [
{
path: ':option',
component: () => import('../components/Option1.vue'),
},
],
Depending on what :option is, I want to import a different component (Option1.vue, Option2.vue, etc.). I know I could put several children but i actually need the option variable in my parent component (I make tests if the route has an option).
How would it be possible to do that?
Thanks in advance :)
You can create a loader component containing a dynamic component instead of doing conditional routing. In the loader, you'll conditionally lazy load the option component based on the route param. Not only is this easier when routing, you also don't have to manually import anything, and only options that are used will be imported.
Step 1. Route to the option loader component
router
{
path: ':option',
component: () => import('../components/OptionLoader.vue'),
}
Step 2. In that option loader template, use a dynamic component which will be determined by a computed called optionComponent:
OptionLoader.vue
<template>
<component :is="optionComponent" />
</template>
Step 3. Create a computed that lazy loads the current option
OptionLoader.vue
export default {
computed: {
optionComponent() {
return () => import(`#/components/Option${this.$route.params.option}.vue`);
}
}
}
This will load the component called "Option5.vue", for example, when the option route param is 5. Now you have a lazy loaded option loader and didn't have to manually import each option.
Edit: OP has now indicated that he's using Vue 3.
Vue 3
For Vue 3, change the computed to use defineAsyncComponent:
OptionsLoader.vue
import { defineAsyncComponent } from "vue";
computed: {
optionComponent() {
return defineAsyncComponent(() =>
import(`#/components/Option${this.$route.params.option}.vue`)
);
}
}
Here is something that works in VueJS3:
<template>
<component :is="userComponent"/>
</template>
<script>
import { defineAsyncComponent } from 'vue';
import { useRoute, useRouter } from 'vue-router';
export default {
computed: {
userComponent() {
const route = useRoute();
const router = useRouter();
const components = {
first: 'Option1',
second: 'Option2',
third: 'OtherOption',
fourth: 'DefaultOption',
};
if (components[route.params.option]) {
return defineAsyncComponent(() => import(`./options/${components[route.params.option]}.vue`));
}
router.push({ path: `/rubrique/${route.params.parent}`, replace: true });
return false;
},
},
};
</script>
Source: https://v3-migration.vuejs.org/breaking-changes/async-components.html
And it's possible to get an error message like this one for the line with "return":
Syntax Error: TypeError: Cannot read property 'range' of null
In that case, it means you probably want to migrate from babel-eslint to #babel/eslint-parser (source: https://babeljs.io/blog/2020/07/13/the-state-of-babel-eslint#the-present)
I have a component called SpotifyButton in the components directory that looks like this:
<template functional>
<b-button pill size="sm" :href="props.spotifyUri" class="spotify-green">
<b-img-lazy
src="~/assets/Spotify_Icon_RGB_White.png"
height="20"
width="20"
/>
View on Spotify
</b-button>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
name: 'SpotifyButton',
props: {
spotifyUri: {
type: String,
required: true
}
}
});
</script>
I'm able to import and use this in a component in the pages directory like so without any problem:
<template>
<spotify-button :spotify-uri="artist.uri"/>
</template>
<script lang="ts">
import Vue from 'vue';
import { Context } from '#nuxt/types';
import FullArtist from '#/types/FullArtist';
import SpotifyButton from '#/components/SpotifyButton.vue';
export default Vue.extend({
name: 'ArtistPage',
components: {
SpotifyButton
},
async asyncData({ $axios, params, error }: Context) {
try {
const artist: FullArtist = await $axios.$get(`/api/artists/${params.id}`);
return { artist };
} catch (e) {
error({ statusCode: 404, message: 'Artist not found' });
}
},
data() {
return {
artist: {
name: ''
} as FullArtist
};
}
});
</script>
However if I try to import SpotifyButton into another component in the components directory in the same way, I get the following error.
Here is the ArtistPreview component, which is in the components directory:
<template functional>
<spotify-button :spotify-uri="props.artist.uri"/>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
import SpotifyButton from '#/components/SpotifyButton.vue';
import SimpleArtist from '#/types/SimpleArtist';
export default Vue.extend({
name: 'ArtistPreview',
components: {
SpotifyButton
},
props: {
artist: {
type: Object as PropType<SimpleArtist>,
required: true
}
}
});
</script>
Am I missing something? Why does an import that works perfectly fine in a pages directory component not work in a components directory component?
This was happening because I'm using functional components. It turns out you can't nest functional components without doing some funky workarounds. Here's the GitHub issue with a few solutions.
I went with the first solution, so my ArtistPreview component now looks something like this:
<template functional>
<spotify-button :spotify-uri="props.artist.uri"/>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
import SpotifyButton from '#/components/SpotifyButton.vue';
import SimpleArtist from '#/types/SimpleArtist';
Vue.component("spotify-button", SpotifyButton);
export default Vue.extend({
name: 'ArtistPreview',
props: {
artist: {
type: Object as PropType<SimpleArtist>,
required: true
}
}
});
</script>
Go with:
import SpotifyButton from '~/components/SpotifyButton.vue'
With Typescript is better use another approach: Add 'nuxt-property-decorator' and follow his flow.
So, you define your component as follow:
<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import SpotifyButton from '~/components/SpotifyButton.vue'
#Component({
components: {
SpotifyButton
},
})
class AnotherComponent extends Vue {
...
}
export default AnotherComponent
</script>
[Nuxt Property Decorator on Github][1]
I think is important to read the official [Nuxt Typescript documentation][2] to a proper setup.
I hope it helps!
[1]: https://github.com/nuxt-community/nuxt-property-decorator
[2]: https://typescript.nuxtjs.org/
I want to separate my logic from the component in vue.
What I have in mind is this:
One file for HTML template, the data pass as props.
Another file that has a lot of functions and getters that gets the data from the store/API.
So after I search a lot I understand I need something like "hooks in reacts".
What I find is u3u/vue-hooks.
My question is what are the benefits to use this idea/library? cause it seems like I do the same with and without vue-hooks.
for example:
foo.ts:
import { computed, defineComponent } from '#vue/composition-api';
export default defineComponent({
name: 'foo',
props: {
icon: {
type: String,
default: '',
},
},
setup(props) {
const iconName = computed(() => props.icon);
return {
iconName,
};
},
});
and foo.vue:
<template>
<div>{{iconName}}</div>
</template>
<script lang="ts" src="./foo.ts">
</script>
<style lang="scss">
</style>
So until here, I separate the logic from the component and I can choose which file to attach the view.
But I can do it with the class as well.
just to change the foo.ts file to:
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class Foo extends Vue {}
I would like it if anyone explains to me - if hooks are the way to separate the logic from the UI? and should I use vue-hooks to do it?
Please pardon my syntax, I'm new to vue.js and may not be getting the terms correct.
I've got a single file component (SFC) named CreateTodo.vue. I've given it the name 'create-todo-item' (in the name property). When I import it in my app.vue file, I can only use the component if I use the markup <create-todo>. If I use <create-todo-item>, the component won't render on the page.
I've since learned that I can do what I want if I list the component in my app.vue in the format components: { 'create-todo-item': CreateTodo } instead of components: { CreateTodo }.
My question is this: is there any point to giving the component a name in the name property? It's not being honored in the consumer, and if I leave it empty, the app runs without error.
Also, am I correct in my belief that vue-loader is assigning the kebab-case element name for template use based on the PascalCase import statement?
Bad - component name property
Here's the code where I try to name the SFC (CreateTodo.vue)
<script>
export default {
name: 'create-todo-item',
data() {
return {
titleText: '',
projectText: '',
isCreating: false,
};
},
};
</script>
The name as listed in the component is ignored by my App.vue. The html renders fine even though I have the element <create-todo> instead of <create-todo-item>:
<template>
<div>
<!--Render the TodoList component-->
<!--TodoList becomes-->
<todo-list v-bind:todos="todos"></todo-list>
<create-todo v-on:make-todo="addTodo"></create-todo>
</div>
</template>
<script>
import TodoList from './components/TodoList.vue'
import CreateTodo from './components/CreateTodo.vue'
export default {
name: 'app',
components: {
TodoList,
CreateTodo,
},
// data function avails data to the template
data() {
return {
};
},
methods: {
addTodo(todo) {
this.todos.push({
title: todo.title,
project: todo.project,
done: false,
});
},
}
};
</script>
Good - don't use component name property at all
Here's my CreateTodo.vue without using the name property:
<script>
export default {
data() {
return {
titleText: '',
projectText: '',
isCreating: false,
};
},
};
</script>
And here's my App.vue using the changed component:
<template>
<div>
<!--Render the TodoList component-->
<!--TodoList becomes-->
<todo-list v-bind:todos="todos"></todo-list>
<create-todo-item v-on:make-todo="addTodo"></create-todo-item>
</div>
</template>
<script>
import TodoList from './components/TodoList.vue'
import CreateTodo from './components/CreateTodo.vue'
export default {
name: 'app',
components: {
TodoList,
'create-todo-item': CreateTodo,
},
// data function avails data to the template
data() {
return {
};
},
methods: {
addTodo(todo) {
this.todos.push({
title: todo.title,
project: todo.project,
done: false,
});
},
}
};
</script>
First note that the .name property in a SFC module is mostly just a convenience for debugging. (It's also helpful for recursion.) Other than that, it doesn't really matter when you locally register the component in parent components.
As to the specific details, in the first example, you're using an ES2015 shorthand notation
components: {
TodoList,
CreateTodo,
},
is equivalent to
components: {
'TodoList': TodoList,
'CreateTodo': CreateTodo
},
so that the component that is imported as CreateTodo is given the name 'CreateTodo' which is equivalent to <create-todo>.
In the second example, you give the name explicitly by forgoing the shorthand notation
components: {
TodoList,
'create-todo-item': CreateTodo,
},
That's equivalent, btw to
components: {
TodoList,
'CreateTodoItem': CreateTodo,
},
So you can see, in that case, that you're giving the component the name 'CreateTodoItem' or, equivalently, <create-todo-item>