How to expand all item in a TreeSelect from Sakai PrimeVUE? - vue.js

I'm using TreeSelect from Sakai PrimeVUE template, I would like to keep all items expanded on open the component, but could not find this option in the documentation.
My TreeSelect implementation:
<TreeSelect v-model="product.category" :overlayVisible="true" selectionMode="single" :options="categories" #change="categoryChange(product)"></TreeSelect>
Info from my package.json:
"primevue": "^3.11.0",
"vue": "3.2.9",

TreeSelect has a method named expandPath(path), where path is the key property in a tree node.
To expand all nodes, collect all the keys from the tree nodes, and then pass each of them to expandPath():
Add a template ref on <TreeSelect> to use it later in script:
<script setup>
import { ref } from 'vue'
const treeRef = ref()
const treeNodes = ref()
</script>
<template>
<TreeSelect ref="treeRef" :nodes="treeNodes" ⋯ />
</template>
Create a method (e.g., named getKeysFromTree) to extract the keys from an array of tree nodes:
const getKeysFromNode = (node) => {
const keys = [node.key]
if (node.children) {
keys.push(...node.children.flatMap(getKeysFromNode))
}
return keys
}
const getKeysFromTree = (tree) => tree.flatMap(getKeysFromNode)
In an onMounted() hook, use the template ref to call expandPath() on each of the tree's keys (extracted with getKeysFromTree() defined above):
<script setup>
import { onMounted } from 'vue'
onMounted(() => {
const data = /* tree nodes data */
treeNodes.value = data
getKeysFromTree(data).forEach((key) => treeRef.value.expandPath(key))
})
</script>
demo

Related

Is there a 'simple' way to dynamically render views in vue?

Let's take a step back and look at the use case:
You're defining a modular interface, and any module that implements it must be able to 'render itself' into the application given a slot and a state.
How do you do it in vue?
Example solution
Let's have a look at the most basic implementation I can assemble:
(full example:
https://stackblitz.com/edit/vitejs-vite-8zclnp?file=src/App.vue)
We have a layout:
# Layout.vue
<template>
<div>
<hr />
<slot name="moduleView" />
<hr />
</div>
</template>
...and an app with a module:
# App.vue
<script lang="ts" setup>
import type { MyModuleState } from "./MyModule";
import Layout from "./Layout.vue";
import { ref } from "vue";
import { MyModule } from "./MyModule";
import ModView from "./ModView.vue";
const state = ref<MyModuleState>({ value: 0 });
const module = new MyModule();
const onClick = () => {
state.value = { value: state.value.value + 1 };
};
const renderModule = () => {
return module.view(state.value);
};
</script>
<template>
<div>currentValue: {{ state.value }}</div>
<div>update: <button #click="onClick">++</button></div>
<div>
<Layout>
<template v-slot:moduleView>
<mod-view :render="renderModule" :state="state" /> // <--- But this!
</template>
</Layout>
</div>
</template>
...but rendering into the slot requires a lot of jumping through obscure hoops:
# ModView.vue
<script lang="ts" setup>
import ModRender from "./ModRender";
import { ref, watch } from "vue";
import type { VNode } from "vue";
const props = defineProps<{
render: (state?: any) => VNode | Array<VNode>;
state?: any;
}>();
const nodes = ref(props.render(props.state));
watch( // <-- Obscure! The view won't update unless you explicitly watch props?
() => props.state,
(nextState) => {
nodes.value = props.render(nextState);
}
);
</script>
<template>
<mod-render :nodes="nodes" />
</template>
# ModRender.ts
import type { VNode } from "vue";
const ModRender = (props: { nodes: VNode | Array<VNode> }) => {
return props.nodes;
};
ModRender.props = {
nodes: {
required: true,
},
};
export default ModRender; // <--- Super obscure, why do you need a functional component for this?
Before we can define the actual module:
# MyModule.ts
import type { VNode } from "vue";
import { h } from "vue";
import ModuleView from "./MyModuleDisplay.vue";
interface AbstractModule<T> {
view: (state: T) => VNode;
}
export interface MyModuleState {
value: number;
}
export class MyModule implements AbstractModule<MyModuleState> {
view(state: MyModuleState): VNode {
return h(ModuleView, { state });
}
}
...and a component for it:
# MyModuleView.vue
<script setup lang="ts">
import type { MyModuleState } from "./MyModule";
const props = defineProps<{ state: MyModuleState }>();
</script>
<template>
<div>{{ state.value }}</div>
</template>
What.
This seems extremely obtuse and verbose.
Am I missing something?
In other component systems an implementation might look like:
export class MyModule implements AbstractModule<MyModuleState> {
view(state: MyModuleState): VNode {
return (<div>{state.value}</div>);
}
}
...
<div>
<Layout>{renderModule(state)}</Layout>
</div>
It seems surprising that so many wrappers and hoops have to be done in vue to do this, which makes me feel like I'm missing something.
Is there an easier way of doing this?
Vnode objects cannot be rendered in component templates as is and need to be wrapped in a component like ModRender. If they are used as universal way to exchange template data in the app, that's a problem. Vnodes still can be directly used in component render functions and functional components with JSX or h like <Layout>{renderModule(state)}</Layout>, this limits their usage.
AbstractModule convention may need to be reconsidered if it results in unnecessary code. Proceed from the fact that a "view" needs to be used with dynamic <component> at some point, and it will be as straightforward as possible.
There may be no necessity for "module" abstraction, but even if there is, module.view can return a component (functional or stateful) instead of vnodes. Or it can construct a component and make it available as a property, e.g.:
class MyModule {
constructor(state) {
this.viewComponent = (props) => h(ModuleView, { state, ...props })
}
}

Vue 3: component `:is` in for loop fails

I'm trying to loop over a list of component described by strings (I get the name of the component from another , like const componentTreeName = ["CompA", "CompA"].
My code is a simple as:
<script setup>
import CompA from './CompA.vue'
import { ref } from 'vue'
// I do NOT want to use [CompA, CompA] because my inputs are strings
const componentTreeName = ["CompA", "CompA"]
</script>
<template>
<h1>Demo</h1>
<template v-for="compName in componentTreeName">
<component :is="compName"></component>
</template>
</template>
Demo here
EDIT
I tried this with not much success.
Use resolveComponent() on the component name to look up the global component by name:
<script setup>
import { resolveComponent, markRaw } from 'vue'
const myGlobalComp = markRaw(resolveComponent('my-global-component'))
</script>
<template>
<component :is="myGlobalComp" />
<template>
demo 1
If you have a mix of locally and globally registered components, you can use a lookup for local components, and fall back to resolveComponent() for globals:
<script setup>
import LocalComponentA from '#/components/LocalComponentA.vue'
import LocalComponentB from '#/components/LocalComponentB.vue'
import { resolveComponent, markRaw } from 'vue'
const localComponents = {
LocalComponentA,
LocalComponentB,
}
const lookupComponent = name => {
const c = localComponents[name] ?? resolveComponent(name)
return markRaw(c)
}
const componentList = [
'GlobalComponentA',
'GlobalComponentB',
'LocalComponentA',
'LocalComponentB',
].map(lookupComponent)
</script>
<template>
<component :is="c" v-for="c in componentList" />
</template>
demo 2
Note: markRaw is used on the component definition because no reactivity is needed on it.
When using script setup, you need to reference the component and not the name or key.
To get it to work, I would use an object where the string can be used as a key to target the component from an object like this:
<script setup>
import CompA from './CompA.vue'
import { ref } from 'vue'
const components = {CompA};
// I do NOT want to use [CompA, CompA] because my inputs are strings
const componentTreeName = ["CompA", "CompA"]
</script>
<template>
<h1>Demo</h1>
<template v-for="compName in componentTreeName">
<component :is="components[compName]"></component>
</template>
</template>
To use a global component, you could assign components by pulling them from the app context. But this would require the app context to be available and the keys known.
example:
import { app } from '../MyApp.js'
const components = {
CompA: app.component('CompA')
}
I haven't tested this, but this might be worth a try to check with getCurrentInstance
import { ref,getCurrentInstance } from 'vue'
const components = getCurrentInstance().appContext.components;

How Do I Share Async API Data across Components in Vue 3?

I have a top-level component that gets data from an API at regular intervals. I want to make a single API request and get all the data for my app in one place to reduce the number of requests to the API server. (FYI, my project looks like it's using Typescript but I'm not yet.)
Everything works fine in my top-level component:
//Parent
<script lang="ts">
import { defineComponent, ref, provide, inject, onMounted } from 'vue'
import getData from '#/data.ts'
export default defineComponent({
setup(){
const workspaces = ref([])
onMounted(async () => {
let api = inject('api') //global var from main.ts
let data = await getData(api) //API request inside data.ts
console.log(data.workspaces) //<-- data looks good here
workspaces.value = data.workspaces
//Trying to share workspaces with other components
provide('workspaces', data.workspaces)
})
return {
workspaces
}
}
})
</script>
<template>
{{ workspaces}} <!-- workspaces render fine here -->
</template>
But my child can't use the provide data via inject:
//Child
<script lang="ts">
import { defineComponent, inject, onMounted, ref } from 'vue'
export default defineComponent({
setup(){
let workspaces = ref([])
onMounted(async () => {
workspaces.value = await inject('workspaces') //<-- just a guess; doesn't work
})
return{
workspaces
}
}
})
</script>
<template>
{{ workspaces }} <!-- nothing here -->
</template>
I've made a couple assumptions as to the cause of the problem:
The Child component loads before the parent's async stuff is done, and is therefore empty.
I probably can't use project/inject in async scenarios like this.
So how can I share async data from an API across components in my app? Is my only option to go back to old-school props and pass the data down manually?
provide/inject are misused and subject to race conditions. Composition API is generally supposed to be at used on component initialization (setup, before any await) and not in onMounted. Even if there weren't such restriction, onMounted in parent component runs after the one in child component and can't provide a value at the time when a child is mounted.
The purpose of refs is to provide a reference to a value that can be changed later, so it could be passed by reference and stay reactive, this property isn't currently used.
It should be in parent component:
setup(){
const workspaces = ref([])
provide('workspaces', workspaces)
let api = inject('api')
onMounted(async () => {
let data = await getData(api)
workspaces.value = data.workspaces
})
return { workspaces }
In child component:
setup(){
let workspaces = inject('workspaces')
return { workspaces }

Auto Import Components Inside A Component In Vue JS

I would like to know best practices to auto import components inside another component in vue 3.
Auto Import all components from src/components directory that start with "Base" e.g. BaseInput.vue, BaseSelect.vue etc. (Project Created Using Vue CLI)
src/views/EventCreate.vue
<template>
//template data here
</template>
<script>
import upperFirst from "lodash/upperFirst";
import camelCase from "lodash/camelCase";
//require.context function from webpack
const requireComponents = require.context(
"../components",
false,
/Base[A-Z]\w+\.(vue|js)$/
);
let comps = {};
requireComponents.keys().forEach((fileName) => {
//removing extension converting to valid component name e.g. "./base-input.vue" to "BaseInput"
const componentConfig = requireComponents(fileName);
const componentName = upperFirst(
camelCase(fileName.replace(/^\.\/(.*)\.\w+$/, "$1"))
);
//add it to comps object with dynamic name
comps[componentName] = componentConfig.default || componentConfig;
});
export default{
components:{ ...comps }
//remaining exports here
}
</script>
<style scoped>
//style here
</style>

can't use template ref on component in vue 3 composition api

I want to get the dimensions of a vue.js component from the parent (I'm working with the experimental script setup).
When I use the ref inside a component, it works as expected. I get the dimensions:
// Child.vue
<template>
<div ref="wrapper">
// content ...
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const wrapper = ref(null)
onMounted(() => {
const rect = wrapper.value.getBoundingClientRect()
console.log(rect) // works fine!
})
</script>
But I want to get the dimension inside the parent component. Is this possible?
I have tried this:
// Parent.vue
<template>
<Child ref="wrapper" />
</template>
<script setup>
import Child from './Child'
import { ref, onMounted } from 'vue'
const wrapper = ref(null)
onMounted(() => {
const rect = wrapper.value.getBoundingClientRect()
console.log(rect) // failed!
})
</script>
the console logs this error message:
Uncaught (in promise) TypeError: x.value.getBoundingClientRect is not a function
In the documentation I can only find the way to use template refs inside the child component
does this approach not work because the refs are "closed by default" as the rfcs description says?
I ran into this issue today. The problem is that, when using the <script setup> pattern, none of the declared variables are returned. When you get a ref to the component, it's just an empty object. The way to get around this is by using defineExpose in the setup block.
// Child.vue
<template>
<div ref="wrapper">
<!-- content ... -->
</div>
</template>
<script setup>
import { defineExpose, ref } from 'vue'
const wrapper = ref(null)
defineExpose({ wrapper })
</script>
The way you set up the template ref in the parent is fine. The fact that you were seeing empty object { } in the console means that it was working.
Like the other answer already said, the child ref can be accessed from the parent like this: wrapper.value.wrapper.getBoundingClientRect().
The rfc has a section talking about how/why this works: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md#exposing-components-public-interface
It's also important to note that, with the <script setup> pattern, your ref in the parent component will not be a ComponentInstance. This means that you can't call $el on it like you might otherwise. It will only contain the values you put in your defineExpose.
I don't this this is necessarily related to the <script setup> tag. Even in the standard script syntax your second example will not work as-is.
The issue is you are putting ref directly on the Child component:
<template>
<Child ref="wrapper" />
</template>
and a ref to a component is NOT the same as a ref to the root element of that component. It does not have a getBoundingClientRect() method.
In fact, Vue 3 no longer requires a component to have a single root element. You can define your Child component as :
<template>
<div ref="wrapper1">// content ...</div>
<div ref="wrapper2">// content ...</div>
</template>
<script >
import { ref } from "vue";
export default {
name: "Child",
setup() {
const wrapper1 = ref(null);
const wrapper2 = ref(null);
return { wrapper1, wrapper2 };
},
};
</script>
What should be the ref in your Parent component now?
Log the wrapper.value to your console from your Parent component. It is actually an object of all the refs in your Child component:
{
wrapper1: {...}, // the 1st HTMLDivElement
wrapper2: {...} // the 2nd HTMLDivElement
}
You can do wrapper.value.wrapper1.getBoundingClientRect(), that will work fine.
You could get access to the root element using $el field like below:
<template>
<Child ref="wrapper" />
</template>
<script setup>
import Child from './Child'
import { ref, onMounted } from 'vue'
const wrapper = ref(null)
onMounted(() => {
const rect = wrapper.value.$el.getBoundingClientRect()
console.log(rect)
})
</script
Right, so here's what you need to do:
// Parent component
<template>
<Child :get-ref="(el) => { wrapper = el }" />
</template>
<script setup>
import Child from './Child.vue';
import { ref, onMounted } from 'vue';
const wrapper = ref();
onMounted(() => {
const rect = wrapper.value.getBoundingClientRect()
console.log(rect) // works fine!
});
</script>
and
// Child component
<template>
<div :ref="(el) => { wrapper = el; getRef(el)}">
// content ...
</div>
</template>
<script setup>
import { defineProps, ref, onMounted } from 'vue';
const props = defineProps({
getRef: {
type: Function,
},
});
const wrapper = ref();
onMounted(() => {
const rect = wrapper.value.getBoundingClientRect()
console.log(rect) // works fine!
});
</script>
To learn why, we need to check Vue's documentation on ref:
Vue special-attribute 'ref'.
On dynamic binding of (template) ref, it says:
<!-- When bound dynamically, we can define ref as a callback function,
passing the element or component instance explicitly -->
<child-component :ref="(el) => child = el"></child-component>
Since the prop lets you pass data from the parent to a child, we can use the combination of the prop and dynamic ref binding to get the wanted results. First, we pass the dynamic ref callback function into the child as the getRef prop:
<Child :get-ref="(el) => { wrapper = el }" />
Then, the child does the dynamic ref binding on the element, where it assigns the target el to its wrapper ref and calls the getRef prop function in that callback function to let the parent grab the el as well:
<div :ref="(el) => {
wrapper = el; // child registers wrapper ref
getRef(el); // parent registers the wrapper ref
}">
Note that this allows us to have the ref of the wrapper element in both the parent AND the child component. If you wished to have access to the wrapper element only in the parent component, you could skip the child's callback function, and just bind the ref to a prop like this:
// Child component
<template>
<div :ref="getRef">
// content ...
</div>
</template>
<script setup>
import { defineProps } from 'vue';
const props = defineProps({
getRef: {
type: Function,
},
});
</script>
That would let only the parent have the ref to your template's wrapper.
If you're seeing the wrapper.value as null then make sure the element you're trying to get the ref to isn't hidden under a false v-if. Vue will not instantiate the ref until the element is actually required.
I realize this answer is not for the current question, but it is a top result for "template ref null vue 3 composition api" so I suspect more like me will come here and will appreciate this diagnosis.