I have a component called x-input that works as following:
<x-input k="name" v-model="r"></x-input>
<x-input k="name" :modelValue="r"></x-input>
This works fine and it is reactive.
How do I create the same functionality using a render function with Composition API and <script setup>?
Here is what i am trying but doesn't work:
<script setup>
import { h, ref} from 'vue'
const r = ref("test")
const entity = h(resolveComponent('x-input'), {k: "name", modelValue: r }, {})
</script>
<template>
<entity />
</template>
Here entity is not reactive.
Where do you define/import your x-input component?
If you import it, then you don't need to resolve it.
<script setup>
import { h, ref, resolveComponent, } from 'vue'
import xInput from './xInput.vue'
const k = ref("name")
const r = ref("test")
const update = () => {
k.value = "name new"
r.value = "test new"
}
const entity = h('div',
[ h('p', 'Entity: '),
h(xInput, {k: k, modelValue: r }),
h('button', { onClick: () => { update() } }, 'update')
])
</script>
<template>
<entity />
</template>
Here is the working SFC Playground
You don't need to resolve your component. Just use the component itself.
h(xInput, {k: k, modelValue: r }, {}),
resolveComponent() is great when you have dynamic component names.
Check: https://stackoverflow.com/a/75405334/2487565
Here is the working playground
const { createApp, h, ref, resolveComponent } = Vue;
const xInput = {
name: 'x-input',
props: ['k', 'modelValue'],
template: '<div>k: {{k}}<br/>modelValue: {{modelValue}}<br/></div>'
}
const Entity = {
components: {
xInput
},
setup(props, { attrs, slots, emit, expose } ) {
const r = ref("test")
const k = ref("name")
const update = () => {
k.value = "name new"
r.value = "test new"
}
return () => h('div',
[
// You don't need to resolve your component
h(xInput, {k: k, modelValue: r }, {}),
h(resolveComponent('x-input'), {k: k, modelValue: r }, {}),
h('button', { onClick: () => { update() } }, 'update')
])
}
}
const app = createApp({ components: { Entity } })
app.mount('#app')
#app { line-height: 1.5; }
[v-cloak] { display: none; }
<div id="app">
<entity></entity>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
Related
I have a question regarding Storybook and Vue components with v-models. When writing a story for let's say an input component with a v-model i want a control reflecting the value of this v-model. Setting the modelValue from the control is no problem, but when using the component itself the control value stays the same. I am searching the web for a while now but i can't seem to find a solution for this.
A small example:
// InputComponent.vue
<template>
<input
type="text"
:value="modelValue"
#input="updateValue"
:class="`form-control${readonly ? '-plaintext' : ''}`"
:readonly="readonly"
/>
</template>
<script lang="ts">
export default {
name: "GcInputText"
}
</script>
<script lang="ts" setup>
defineProps({
modelValue: {
type: String,
default: null
},
readonly: {
type: Boolean,
default: false
}
});
const emit = defineEmits(['update:modelValue']);
const updateValue = (event: Event) => {
const target = event.target as HTMLInputElement;
emit('update:modelValue', target.value);
}
</script>
In Storybook:
Does anyone have a solution to make this working?
Thanks in advance!
In my case, I have a custom select input that uses a modelValue prop.
I tried this and worked for me:
at my-component.stories.js:
import { ref } from 'vue'
import MyComponent from './MyComponent.vue'
export default {
title: 'Core/MyComponent',
component: MyComponent,
argTypes: { }
}
const Template = (args) => ({
components: { MyComponent },
setup() {
let model = ref('Javascript')
const updateModel = (event) => model.value = event
return { args, model, updateModel }
},
template: '<my-component v-bind="args" :modelValue="model" #update:modelValue="updateModel" />'
})
export const Default = Template.bind({})
Default.args = {
options: [
'Javascript',
'PHP',
'Java'
]
}
I don't succeed to use a Pinia getter properly in the setup part.
It's working when I use the getter directly in the part, but not in the part.
Here is my (simplified) pinia store :
export const useCharactersStore = () => {
const charactersStoreBuilder = defineStore("characters", {
state: (): State => ({
characters: {},
}),
getters: {
getCharacter(state) {
return (characterId: string) => state.characters[characterId];
},
},
actions: {
async fetchCharacters() {
this.characters = {
1: "Character 1",
2: "Character 2",
};
},
},
});
const charactersStore = charactersStoreBuilder();
charactersStore.fetchCharacters(); // async fetching
return charactersStore;
And here is my usage :
<script setup>
import { useCharactersStore } from "#/stores/characters";
import { storeToRefs } from "pinia";
const characterId = 1;
const characterStore = useCharactersStore();
// Various alternatives :
// Don't work
const { getCharacter } = storeToRefs(characterStore);
const character = getCharacter.value(characterId);
// Don't work
const character = characterStore.getCharacter(characterId);
// Don't work
const character = characterStore.getCharacter(characterId);
// Work
const getCharacter = characterStore.getCharacter;
</setup>
<template>
<pre>
<!-- Don't work -->
{{ character }}
<!-- Work -->
{{ getCharacter(characterId) }}
</pre>
</template>
I've also tried with character as a ref, with its value updated with a watch, and various things. I'm absolutely lost :/
Your question is ambiguous. I do not know what you mean by.
It's working when I use the getter directly in the part, but not in the part.
However, I made a slight modification to your code. For me, this works.
// CharactersStore.ts
import { defineStore } from 'pinia'
export const useCharactersStore = () => {
const charactersStoreBuilder = defineStore('CharactersStore', {
state: () => {
return {
characters: {} as Record<number | string, string | number>,
}
},
actions: {
// async is irrelevant here. You can use setTimeout to simulate async here.
async fetchCharacters() {
this.characters = {
1: 'Character 1',
2: 'Character 2',
}
},
},
getters: {
getCharacterByID: (state) => (characterId: string | number) =>
state.characters[characterId],
},
})
const charactersStore = charactersStoreBuilder()
charactersStore.fetchCharacters() // async fetching
return charactersStore
}
The Test page I used.
<script setup lang="ts">
import { storeToRefs } from 'pinia'
const c = useCharactersStore()
const characterId = 1 // even though it works, It is not reactive.
const character = c.getCharacterByID(characterId)
const { getCharacterByID: getCharacterWithStoreRef } = storeToRefs(c)
</script>
<template>
<div>
<h1>Test page</h1>
<div>
<pre>
Get character by Id : {{ c.getCharacterByID(characterId) }}
character {{ character }}
getCharacterWithStoreRef {{ getCharacterWithStoreRef(characterId) }}
</pre>
</div>
</div>
</template>
Result
Test the solution provided
Feel free to let me know if it works for you. Happy coding
Additional information
Read more about Passing arguments to getters in Pinia
I have the following working code for a search input using options API for component data, watch and methods, I am trying to convert that to the composition api.
I am defining props in <script setup> and also a onMounted function.
<template>
<label for="search" class="hidden">Search</label>
<input
id="search"
ref="search"
v-model="search"
class="border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm h-9 w-1/2"
:class="{ 'transition-border': search }"
autocomplete="off"
name="search"
placeholder="Search"
type="search"
#keyup.esc="search = null"
/>
</template>
<script setup>
import {onMounted} from "vue";
const props = defineProps({
routeName: String
});
onMounted(() => {
document.getElementById('search').focus()
});
</script>
<!--TODO convert to composition api-->
<script>
import { defineComponent } from "vue";
export default defineComponent({
data() {
return {
// page.props.search will come from the backend after search has returned.
search: this.$inertia.page.props.search || null,
};
},
watch: {
search() {
if (this.search) {
// if you type something in the search input
this.searchMethod();
} else {
// else just give us the plain ol' paginated list - route('stories.index')
this.$inertia.get(route(this.routeName));
}
},
},
methods: {
searchMethod: _.debounce(function () {
this.$inertia.get(
route(this.routeName),
{ search: this.search }
);
}, 500),
},
});
</script>
What I am trying to do is convert it to the composition api. I have tried the following but I can't get it to work at all.
let search = ref(usePage().props.value.search || null);
watch(search, () => {
if (search.value) {
// if you type something in the search input
searchMethod();
} else {
// else just give us the plain ol' paginated list - route('stories.index')
Inertia.get(route(props.routeName));
}
});
function searchMethod() {
_.debounce(function () {
Inertia.get(
route(props.routeName),
{search: search}
);
}, 500)
}
Any help or pointers in how to convert what is currently in <script> into <script setup> would be greatly appreciated thanks.
I managed to get this working with the below!
<script setup>
import {onMounted, ref} from "vue";
import {Inertia} from "#inertiajs/inertia";
const props = defineProps({
route_name: {
type: String,
required: true
},
search: {
type: String,
default: null
}
});
const search = ref(props.search);
onMounted(() => {
search.value.focus();
search.value.addEventListener('input', () => {
if (search.value.value) {
searching();
} else {
Inertia.get(route(props.route_name));
}
});
});
const searching = _.debounce(function() {
Inertia.get(route(props.route_name), {search: search.value.value});
}, 500);
</script>
A simple working example of a Vue2 dynamic component
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
export default {
data: () => ({
isShow: false
}),
computed: {
name() {
return this.isShow ? () => import('./DynamicComponent') : '';
}
},
methods: {
onClick() {
this.isShow = true;
}
},
}
</script>
Everything works, everything is great. I started trying how it would work with the Composition API.
<template>
<div>
<h1>O_o</h1>
<component :is="state.name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
import {ref, reactive, computed} from 'vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? import('./DynamicComponent.vue') : '')
});
const isShow = ref(false);
const onClick = () => {
isShow.value = true;
}
return {
state,
onClick
}
}
}
</script>
We launch, the component does not appear on the screen, although no errors are displayed.
You can learn more about 'defineAsyncComponent' here
https://labs.thisdot.co/blog/async-components-in-vue-3
or on the official website
https://v3.vuejs.org/api/global-api.html#defineasynccomponent
import { defineAsyncComponent, defineComponent, ref, computed } from "vue"
export default defineComponent({
setup(){
const isShow = ref(false);
const name = computed (() => isShow.value ? defineAsyncComponent(() => import("./DynamicComponent.vue")): '')
const onClick = () => {
isShow.value = true;
}
}
})
Here is how you can load dynamic components in Vue 3. Example of dynamic imports from the icons collection inside /icons folder prefixed with "icon-".
BaseIcon.vue
<script>
import { defineComponent, shallowRef } from 'vue'
export default defineComponent({
props: {
name: {
type: String,
required: true
}
},
setup(props) {
// use shallowRef to remove unnecessary optimizations
const currentIcon = shallowRef('')
import(`../icons/icon-${props.name}.vue`).then(val => {
// val is a Module has default
currentIcon.value = val.default
})
return {
currentIcon
}
}
})
</script>
<template>
<svg v-if="currentIcon" width="100%" viewBox="0 0 24 24" :aria-labelledby="name">
<component :is="currentIcon" />
</svg>
</template>
You don't need to use computed or watch. But before it loads and resolved there is nothing to render, this is why v-if used.
UPD
So if you need to change components (icons in my case) by changing props use watchEffect as a wrapper around the import function.
watchEffect(() => {
import(`../icons/icon-${props.name}.vue`).then(val => {
currentIcon.value = val.default
})
})
Don't forget to import it from vue =)
The component should be added to components option then just return it name using the computed property based on the ref property isShow :
components:{
MyComponent:defineAsyncComponent(() => import("./DynamicComponent.vue"))
},
setup(){
const isShow = ref(false);
const name = computed (() => isShow.value ? 'MyComponent': '')
const onClick = () => {
isShow.value = true;
}
}
Instead of string you should provide Component
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>
I'm having issues getting complete coverage on my apollo components. istanbul is reporting the functions inside compose() are not getting called. These are Redux connect() functions and apollo graph() functions.
export default compose (
...
connect(mapStateToProps, mapDispatchToProps), // <-- functions not covered
graphql(builderQuery, {
options: (ownProps) => { // <-- function not covered
...
)(ComponentName);
I'm mounting using enzyme, trying to do something similar to the react-apollo example.
const mounted = shallow(
<MockedProvider mocks={[
{ request: { query, variables }, result: { data: response.data } }
]}>
<ConnectedComponentName />
</MockedProvider>
);
The only way I've been able to achieve 100% coverage is if I export all of the functions and call them directly.
testing composed graphql/redux containers
Try something like this for your setup:
// mocks.js
import configureMockStore from 'redux-mock-store'
import { ApolloClient } from 'react-apollo'
import { mockNetworkInterface } from 'apollo-test-utils'
export const mockApolloClient = new ApolloClient({
networkInterface: mockNetworkInterface(),
})
export const createMockStore = configureMockStore()
This will set you up to properly test your containers:
// container-test.js
import { mount } from 'enzyme'
import { createMockStore, mockApolloClient } from 'mocks'
beforeEach(() => {
store = createMockStore(initialState)
wrapper = mount(
<ApolloProvider client={mockApolloClient} store={store}>
<Container />
</ApolloProvider>
)
})
it('maps state & dispatch to props', () => {
const props = wrapper.find('SearchResults').props()
const expected = expect.arrayContaining([
// These props come from an HOC returning my grapqhql composition
'selectedListing',
'selectedPin',
'pathname',
'query',
'bbox',
'pageNumber',
'locationSlug',
'selectListing',
'updateCriteria',
'selectPin',
])
const actual = Object.keys(props)
expect(actual).toEqual(expected)
})
testing graphql options
Because the graphql fn has a signature like graphql(query, config), you can export your config for testing in isolation for more granular coverage.
import { config } from '../someQuery/config'
describe('config.options', () => {
const state = {
bbox: [],
locationSlug: 'foo',
priceRange: 'bar',
refinements: 'baz',
userSort: 'buzz',
}
const results = {
points: [
{ propertyName: 'Foo' },
{ propertyName: 'Bar' },
],
properties: [
{ propertyName: 'Foo' },
{ propertyName: 'Bar' },
],
}
it('maps input to variables', () => {
const { variables } = config.options(state)
const expected = { bbox: [], locationSlug: 'foo', priceRange: 'bar', refinements: 'baz', userSort: 'buzz' }
expect(variables).toEqual(expected)
})
it('returns props', () => {
const response = { data: { loading: false, geo: { results } } }
const props = config.props(response)
expect(props.results).toEqual(results.properties)
expect(props.spotlightPoints).toEqual(results.points)
})
})