How to pass props to custom header component using ag-grid? - vue.js

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.

Related

dynamically highlight block with highlight.js in vue app

I have a VueJS where I have created a component for rendering the contents from a WYSIWYG component (tiptap).
I have the following content being returned from the backend
let x = 0;
enum A {}
function Baa() {}
I'm using highlight.js to highlight this code snippet in the following manner:
import { defineComponent, h, nextTick, onMounted, onUpdated, ref, watch } from 'vue';
// No need to use a third-party component to highlight code
// since the `#tiptap/extension-code-block-lowlight` library has highlight as a dependency
import highlight from 'highlight.js'
export const WYSIWYG = defineComponent({
name: 'WYSIWYG',
props: {
content: { type: String, required: true },
},
setup(props) {
const root = ref<HTMLElement>(null);
const highlightClass = 'hljs';
const hightlightCodes = async () => {
console.log(root.value?.querySelectorAll('pre code')[0]);
setTimeout(() => {
root.value?.querySelectorAll('pre code').forEach((el: HTMLElement) => {
highlight.highlightElement(el as HTMLElement);
});
}, 2000);
}
onMounted(hightlightCodes);
watch(() => props.content, hightlightCodes);
return function render() {
return h('div', {
class: 'WYSIWYG',
ref: root,
innerHTML: props.content
});
};
},
});
Now, when I visit the page by typing the URL in the browser, it highlights the typescript code
Whenever I visit a different page and click on my browser's "Go back" button, it makes the code completely vanishes
What I have tried
I can see that the line root.value?.querySelectorAll('pre code') is returning the correct items and the correct code is present but the code vanishes after the 2 seconds passes - due to setTimeout.
How can I make highlight.js highlight the code parts whenever props.content changes?
Option 1
Use Highlight.js Vue integration (you need to setup the plugin first, check the link):
<script setup>
const props = defineProps({
content: { type: String, required: true },
})
</script>
<template>
<highlightjs :code="content" language="ts" />
</template>
Option 2
Use computed to reactively compute highlighted HTML of props.content
Use sync highlight(code, options) function to get the highlighted HTML
Use HTML as-is via innerHTML prop or v-html directive
<script setup>
import { computed } from 'vue'
import highlight from 'highlight.js'
const props = defineProps({
content: { type: String, required: true },
})
const html = computed(() => {
const { value } = highlight.highlight(props.content, { lang: 'ts' })
return value
})
</script>
<template>
<div v-html="html" />
</template>

vue3: extends missing parent's setup

parent.vue
<template>
<input :type="computedType"/>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
type: {
required: false,
type: String,
default: 'text',
},
});
const showPassword = ref(false);
const computedType = computed(() => {
if (props.type === 'password') {
return showPassword.value ? 'text' : 'password';
}
return props.type;
});
</script>
<script>
export default {
data() {
return {
uuid: getRandomUuid(),
}
}
}
</script>
child.vue
<template>
<input :type="computedType"/>
</template>
<script>
import Parent from '~/components/parent.vue'
export default {
extends: Parent
}
</script>
In Vue3, I have a child.vue component which is inherited from parent.vue, I defined a computedType computed attribute in parent.vue, but it's missing in child.vue, however, uuid which is defined in parent.vue is accessible in child.vue.
[Vue warn]: Property "computedType" was accessed during render but is not defined on instance.
How to get computedType and any other attributes defined in <script setup> of parent.vue in child.vue?
Really appreciate any help provided!
update:
Only if I define all the attributes in <script>(but not in setup()) instead of <script setup>, they could be accessible
There are a few specific instances where you can have more than one script tag, and they are all outlined here in the documentation. Still, besides those 3 specific use cases, you shouldn't use more than one script tag. Your case of a separate script tag for one data variable is not a valid use case.
I recommend writing the component entirely with the options API (the only API that supports the "extends" option), or writing entirely with the composition API where you would use composable functions to effectively extend your component
defineExpose({computedType});
try this

Vue3 - after provide/inject object inside component is loosing data

I'm having small issue with provide/inject in my project.
In App.vue, I'm pulling data from DB and pushing it into object. With console log I checked and all data it's there.
<template>
<router-view />
</template>
<script>
export default {
provide() {
return {
user: this.user,
};
},
data() {
return {
user: '',
};
},
methods: {
///pulling data from DB
func() {
fetch("url")
.then((response) => {
if (response.ok) {
return response.json();
}
})
.then((data) => {
const user = [];
for (const id in data) {
user.push({
id: data[id].user_id,
firstName: data[id].user_firstname,
lastName: data[id].user_lastname,
email: data[id].user_email,
phone: data[id].user_phone,
address1: data[id].user_address_1,
address2: data[id].user_address_2,
address3: data[id].user_address_3,
address4: data[id].user_address_4,
group: data[id].user_group,
});
}
this.user = user;
})
.catch((error) => {
console.log(error);
});
},
},
created() {
this.func();
},
};
</script>
Console log of object user App.vue
Object { id: "3", firstName: "test", lastName: "test", … }
Next I'm injecting it into component. Object inside component exists, but empty - all data cease to exist.
<script>
export default {
inject: ["user"],
};
</script>
console log of object user in component
<empty string>
While in App.vue data is still there, in any components object appears to be empty, but it is there. Any idea why?
Thanks for help.
In short, this happens because you are reassigning user rather than changing user.
Let's say you have a Child component that consumes your inject data and renders the users in a list:
<template>
<div> Child </div>
<ul>
<li v-for="user in users" :key="user.id"> {{user.name}} </li>
</ul>
</template>
<script>
import {inject} from "vue";
export default {
name: "Child",
setup() {
const users = inject("users");
return {users};
}
}
</script>
To provide the users from parent component, all you need to ensure is that users itself is a reactive object, and you keep changing it from the parent rather than reassigning it.
I am going to use the composition api to illustrate what I mean. Compared to options api, everything in composition api is just plain javascript hence there is a lot less behind-the-scene magic. At the end I will tell you how options api is related to the composition api.
<template>
<button #click=generateUsers>
Generate Users
</button>
<Child/>
</template>
<script>
import {reactive, provide, toRefs} from "vue";
import Child from "./Child.vue";
export default {
name: "App",
components: {
Child
},
setup() {
const data = reactive({users: ""});
const generateUsers = () => {
// notice here you are REASSIGNING the users
data.users = [
{id: 1, name: "Alice"}, {id: 2, name: "Bob"}
];
console.log(data.users);
}
// this way of provide will NOT work
provide("users", data.users);
// this way works because of toRefs
const {users} = toRefs(data);
provide("users", users);
return {generateUsers};
}
}
</script>
A few things to note:
the data options in the options api is exactly the same as const data = reactive({users: ""}). Vue will run your data() method, from where you have to return a plain object. And then Vue will automatically call reactive to add reactivity to it.
provide, on the other hand, is not doing any magic - neither in options api, nor in the composition api. It just passes whatever it is given to the consuming component without any massaging.
the reason provide("users", data.users) does not work as you would expect is that the way you populate the users is not a change to the same data.users object (which actually is reactive), but a reassign all together.
the reason toRefs works is because toRefs links to the original parent.
With this understanding in mind, to fix your original code, you just need to ensure you change, instead of reassigning, the users. The simplest way is to define user as an array and push into it when you load data. (in contrast to defining it initially as a string and reassigning it later)
P.S. what also works in composition api, and is a lot simpler, is to:
<template>
<button #click=generateUsers>
Generate Users
</button>
<Child/>
</template>
<script>
import {ref, provide} from "vue";
import Child from "./Child.vue";
export default {
name: "App",
components: {
Child
},
setup() {
const users = ref();
const generateUsers = () => {
// notice here you are not reassigning the users
// but CHANGING its value
users.value = [
{id: 1, name: "Alice"}, {id: 2, name: "Bob"}
];
console.log(users.value);
}
provide("users", users);
return {generateUsers};
}
}
</script>

What are the differences between vue-hooks and vue-property-decorator?

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?

how to create a dynamic vue component that has a computed template containing another component with Object properties without passing it as string

I have a component like this:
Relation.vue
<template>
<div :is="dynamicRelation"></div>
</template>
<script>
import Entry from '#/components/Entry';
import weirdService from '#/services/weird.service';
export default {
name: 'Relation',
data() {
return {
entry1: { type: 'entity', value: 'foo', entity: {id: 4}},
entry2: { type: 'entity', value: 'bar', entity: {id: 5}},
innerText: '#1 wut #2',
}
},
computed: {
dynamicRelation() {
return {
template: `<div>${this.innerText
.replace('#1', weirdService.entryToHtml(this.entry1))
.replace('#2', weirdService.entryToHtml(this.entry2))}</div>`,
name: 'DynamicRelation',
components: { Entry }
};
}
}
}
</script>
wierd.service.js
export default {
entryToHtml(entry) {
[some logic]
return `<entry entry='${JSON.stringify(entry)}'></entry>`;
// unfortunately I cannot return JSX here: <entry entry={entry}></entry>;
// I get 'TypeError: h is not a function'
// unless there is a way to convert JSX to a pure html string on the fly
}
}
Entry.vue
<template>
<div>{{objEntry.name}}</div>
</template>
<script>
export default {
name: 'Entry',
props: {
entry: String // I need this to be Object
},
computed: {
objEntry() {
return JSON.parse(this.entry);
}
}
}
</script>
The innerText property decides how the components will be rendered and it can be changing all the time by having its # slots in any position.
In this example the result is:
<div>
<div>foo</div>
wut
<div>bar</div>
</div>
This works since Entry component has as a property entry that is of type String but I have to JSON.stringify() the entry object in weirdService side and then in Entry component I have to JSON.parse() the string to get the real object back.
How can I make the above work so that I pass an object directly to a dynamic template so I avoid serialization and deserialization all the time.
btw for this to work runtimeCompiler needs to be enabled in vue.config.js:
module.exports = {
runtimeCompiler: true
}
I know I can use JSX to return components with objects in them but this is allowed only in render() function it seems and not custom ones like mine.
Thanks!!
I was able to do what I wanted by using JSON.stringify still but pass the entry as object :entry
wierd.service.js
export default {
entryToHtml(entry) {
return `<entry :entry='${JSON.stringify(entry)}'></entry>`;
}
}
Entry.vue
<template>
<div>{{entry.name}}</div>
</template>
<script>
export default {
name: 'Entry',
props: {
entry: Object
}
}
</script>