I am trying to populate a NativeScript-Vue ListView with templates that contain components of which their types are not known ahead of time. For example, this code does not work as NativeScript does not have a 'component' element but this suggests what I am trying to accomplish:
<ListView for="component in components">
<v-template>
<component :is="component" />
</v-template>
</ListView>
computed: {
components () {
return ['Label', 'Button'];
}
}
Yes, I know you can use if="" in a v-template, but in this case I do not know ahead of time what components need to be loaded in the ListView. In my case I am loading global components in a plugin, and these components will be referenced in the ListView.
Thanks #Manoj. Those wise words made me think, the template can't be dynamic but the contents of the v-template can be. Maybe not for everyone, but this code works for me:
// App.vue
<template>
<Page>
<ActionBar title="Welcome to NativeScript-Vue!"/>
<GridLayout columns="*" rows="400,*">
<ListView ref="lv" for="item in items">
<v-template>
<v-component :type="item.type" :options="item.options" />
</v-template>
</ListView>
</GridLayout>
</Page>
</template>
<script lang="ts">
import Vue from 'nativescript-vue'
import { Component } from 'vue-property-decorator'
import VComponent from './VComponent.vue'
#Component({
components: {
VComponent
}
})
export default class App extends Vue {
get items () {
return [
{type: 'Label', options: [{key: 'text', value: 'I am a Label'}, {key: 'color', value:'red'}] },
{type: 'Button', options: [{key: 'text', value:'I am a Button!'}]}
]
}
}
</script>
// VComponent.vue
<template>
<StackLayout ref="myLayout">
</StackLayout>
</template>
<script lang="ts">
import Vue from 'nativescript-vue'
import { Component, Prop } from 'vue-property-decorator'
import { StackLayout } from 'tns-core-modules/ui/layouts/stack-layout'
import { Label } from 'tns-core-modules/ui/label'
import { Button } from 'tns-core-modules/ui/button'
const classByClassName = {
'Label': Label,
'Button': Button
}
#Component
export default class CoolTemplate extends Vue {
#Prop() type!: string;
#Prop() options;
mounted () {
if (this.type) {
const myLayout = <StackLayout>((<Vue>this.$refs.myLayout).nativeView)
const component = new classByClassName[this.type]
for (var i = 0; i< this.options.length; i++) {
const option = this.options[i];
component[option.key] = option.value
}
myLayout.addChild(component)
}
}
}
</script>
Your template can not be dynamic, that's the whole point of using ListView - keeping them static, so they can be reused as needed. If you like to see different components based on data then you must use multiple templates.
Read more on docs.
Related
Using nuxtjs/svg package, I'm conditionally rendering inline SVGs thus:
<ArrowRight v-if="condition" />
<ExternalLink v-else />
Script:
import ArrowRight from '~/assets/img/arrow-right.svg?inline'
import ExternalLink from '~/assets/img/external-link.svg?inline'
export default {
components: {
ArrowRight,
ExternalLink
}
}
I'd like to make these imports dynamically, but I don't know how in this case partly because of the necessity of the "?inline" part when importing the SVG.
Any idea as to how I can import the SVGs dynamically?
<template>
<div v-html="require(`~/assets/img/${image}.svg?raw`)"/>
</template>
<script>
export default {
computed: {
image() {
return condition ? 'arrow-right' : 'external-link'
}
}
}
</script>
That's one way by using SVGs as raw. But the idea should be clear ;-)
(copied from nuxt svg module readme)
To dynamically import an SVG, you can use the inline require() syntax.
<template>
<div v-html="require(`../assets/${name}.svg?raw`)" />
</template>
<script>
export default {
props: {
name: { type: String, default: "image" },
},
};
</script>
To render an SVG without wrapper element and the use of v-html, a combination of dynamic components and ?inline can be used.
<template>
<component :is="require(`../assets/${name}.svg?inline`)" />
</template>
<script>
export default {
props: {
name: { type: String, default: "image" },
},
};
</script>
I'm writing my first app using NUXT. I'm stuck at this issue for 2 days, so I decided to ask even if I think this is a question with a simple answer (it has to be).
On my project's layouts I have a default.vue and a home.vue
default.vue:
<template>
<div>
<!-- call Header component, this has an nav menu -->
<Header />
<!-- call Hero component -->
<Hero />
<nuxt />
<Footer />
</div>
</template>
<script>
import Header from '~/components/Header.vue'
import Footer from '~/components/Footer.vue'
import Hero from '~/components/Hero.vue'
export default {
components: {
Header,
Footer,
Hero
},
}
</script>
I want to display data from each page (title, subtitle and imageUrl). This data sometimes come from an apollo query request other times are defined on page file.
I've read the docs and searched here for the answer but I wans't able to implement it. I think it has to be done thought Vuex store but I don't know how.
Thank you
You can use nuxtServerInit action in vuex as one way to populate page data.
If you are using nuxt >= 2.12, you can use the new-and-improved fetch hook inside your layouts to make your apollo queries.
I DID IT!
So, it took a time to figure out, but I've learnt a lot during this process.
I'll let here some references I've used to come with this solution.
Very nice article on passing data through props, custom events and Vuex Store
CodeSandBox from Nuxt Documentation.
This question has a method to await apollo data and then render data
Let's go to the way I did it. I don't know if it's the best, but worked like a charm here.
I've created a hero.js file on my store folder:
data: {
title: "",
subtitle: "",
imgUrl: ""
}
})
export const mutations = {
setData (state, obj) {
state.data = {...state.data, ...obj}
}
}
export const getters = {
getHero (state) {
return state.data
}
}
Then on my default.vue I did:
<div>
<!-- call Header component -->
<Header />
<!-- call Hero component with his slots-->
<Hero>
<template v-slot:title>
<h1 class="title">{{ hero.title }}</h1>
</template>
<template v-slot:subtitle>
<h2 class="subtitle">{{ hero.subtitle }}</h2>
</template>
<template v-slot:heroImg>
<img :src="hero.imgUrl" />
</template>
</Hero>
<!-- This is where all yours pages will be -->
<nuxt />
<Footer />
</div>
</template>
<script>
// Import Header component
import Header from '~/components/Header.vue'
import Footer from '~/components/Footer.vue'
import Hero from '~/components/Hero.vue'
import { mapGetters } from 'vuex'
export default {
data(){
return {
//declaring hero Obj to contain hero data
hero: {
title: "",
subtitle: "",
imgUrl: ""
}
}
},
components: {
Header,
Footer,
Hero
},
//Getting getHero getter from hero.js and saving it to newHero
computed: mapGetters({
newHero: 'hero/getHero'
}),
//watching newHero to change and then updating this.hero Obj. This action will update the displayed data
watch: {
newHero: function (obj) {
this.hero = {...this.hero, ...obj}
}
}
}
</script>
Here I declare the variables and store than into Vuex Store:
<template>
...
</template>
<script>
export default {
data() {
return {
hero: {
title: "Awesome Static title",
subtitle: "Awesome static subtitle"
}
}
},
//Saving the declared Hero to Vuex Store, then my default.vue will be able to get it through this.$store.getters
mounted() {
this.$store.commit("hero/setData", this.hero);
},
}
</script>
At some pages the title are fetched from the database (GraphQL using Apollo). Then I did:
<template>
...
</template>
<script>
import getLojaInfo from '~/apollo/queries/loja/loja.gql'
export default {
//declaring data
data() {
return {
lojas: Array,
loading: 0,
hero: {
title: "",
subtitle: "",
imgUrl: ""
}
}
},
//making the query
apollo: {
lojas: {
$loadingKey: 'loading',
prefetch: true,
query: getLojaInfo,
variables () {
return { slug: this.$route.params.singleLoja }
},
//it will wait for query result that and then populate this hero, it will update the hero title, subtitle and image
result(ApolloQueryResult, key) {
this.hero.title = ApolloQueryResult.data.lojas[0].name
this.hero.subtitle = ApolloQueryResult.data.lojas[0].description
this.hero.imgUrl = ApolloQueryResult.data.lojas[0].logo.url
//then commit it to Vuex Store
this.$store.commit("hero/setData", this.hero);
}
},
},
}
</script>
Thank you all, I would appreciate contributions to my code.
I have some global-registered base components to be listed & rendered on the UI as the user drag and drop it on the layout editor. Then, I want it to be processed on a <ComponentRenderer/> component.
Inside of <ComponentRenderer/>, I currently have this kind of logic:
<template>
<div class="component-renderer">
<radio-button
:pageId="pageId"
:input="input"
:preview="preview"
v-if="input.type == 'radio'"
></radio-button>
<check-box
:pageId="pageId"
:input="input"
:preview="preview"
v-if="input.type == 'checkbox'"
></check-box>
<standard-input
:pageId="pageId"
:input="input"
:preview="preview"
v-if="input.type == 'input'"
></standard-input>
...
...
</div>
</template>
Now instead of hard-coding & comparing it manually using v-if, I want it to dynamically compare itself and render it's element and properties so that I don't need to register the other one when a new component was added. Something that looks like this:
<template>
<div class="component-renderer" v-html="preRenderComponent"></div>
</template>
<script>
export default {
props: {
targetedComponent: {
type: Object,
required: true
}
},
computed: {
preRenderComponent() {
return this.$options.components.filter(
component =>
component.extendOptions.name.toLowerCase() == "base" + this.targetedComponent.type.toLowerCase() // E.g: 'input'
);
}
}
};
</script>
Is it possible? And if possible, how could I render the element and properties? Knowing that when I do console.log(Vue.options.components) and exploring it, it does not provide the element that gonna be rendered.
First thing first, I would say thanks for Steven Spungin & Phil for giving an information about this issue.
I tried to implements it on my code and it does work as I expected. For anyone who wondering what is it looks like, here I provides the code that have been implemented.
LayoutRenderer.vue :
<template>
<GridLayout
v-if="pageComponents.length != 0"
:layout.sync="pageComponents"
:col-num="12"
:row-height="30"
:is-draggable="true"
:is-resizable="true"
:is-mirrored="false"
:vertical-compact="true"
:margin="[10, 10]"
:use-css-transforms="true"
:responsive="true"
:auto-size="true"
>
<GridItem
v-for="component in pageComponents"
:key="component.i"
:x="component.x"
:y="component.y"
:w="component.w"
:h="component.h"
:i="component.i"
>
<!-- <ComponentRenderer :component="component"></ComponentRenderer> -->
<Component :is="named(component)"></Component>
</GridItem>
</GridLayout>
</template>
<script>
import VueGridLayout from "vue-grid-layout";
export default {
components: {
GridLayout: VueGridLayout.GridLayout,
GridItem: VueGridLayout.GridItem
},
data() {
return {
pageComponents: []
};
},
created() {
this.fetchComponents();
},
methods: {
fetchComponents() {
let pageId = this.$route.params.component.pageId;
this.$store.dispatch("components/fetchComponents", pageId).then(() => {
this.pageComponents = this.$store.getters["components/components"];
});
},
named(component) {
let componentName = this.$options.filters.capitalize(component.type);
return `Base${componentName}`;
}
}
};
</script>
I have a string (example, because it's an object with many key/values, want to loop and append to htmloutput) with a component name. Is it possible to render/build the component inside a method and display the html output?
Is that possible and how can i achieve that?
<template>
<div v-html="htmloutput"></div>
</template>
<script>
export default {
component: {
ComponentTest
},
data() {
return {
htmloutput: ''
}
},
methods:{
makeHtml(){
let string = 'component-test';//ComponentTest
//render the ComponentTest directly
this.htmloutput = ===>'HERE TO RENDER/BUILD THE COMPONENTTEST'<==
}
},
created(){
this.makeHtml();
}
</script>
You might be looking for dynamic components:
https://v2.vuejs.org/v2/guide/components-dynamic-async.html
Example:
<template>
<component :is="changeableComponent">
</component>
</template>
<script>
import FirstComponent from '#/views/first';
import SecondComponent from '#/views/second';
export default {
components: {
FirstComponent, SecondComponent
},
computed: {
changeableComponent() {
// Return 'first-component' or 'second-component' here which corresponds
// to one of the 2 included components.
return 'first-component';
}
}
}
</script>
Maybe this will help - https://forum.vuejs.org/t/how-can-i-get-rendered-html-code-of-vue-component/19421
StarRating is a sample Vue component. You can get it HTML code by run:
new Vue({
...StarRating,
parent: this,
propsData: { /* pass props here*/ }
}).$mount().$el.outerHTML
in Your method. Remember about import Vue from 'vue'; and of course import component.
What you're trying to do really isn't best practice for Vue.
It's better to use v-if and v-for to conditionally render your component in the <template> section.
Yes you can use the render function for that here is an example :
Vue.component('CompnentTest', {
data() {
return {
text: 'some text inside the header'
}
},
render(createElement) {
return createElement('h1', this.text)
}
})
new Vue({
el: '#app',
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<Compnent-test />
</div>
Or :
if you are using Vue-cli :
on your componentTest component :
export default {
render(createElement) {
return createElement('h1', 'sometext')
}
// Same as
// <template>
// <h1>sometext</h1>
// </template>
//
}
and on your root element (App.vue as default) :
export default {
....
component: {
ComponentTest
}
}
<template>
<div>
....
<Component-test />
</div>
</template>
example : codesandbox
you can read more about
Render Functions & JSX
I don't have a whole vue app, so I use custom elements to replace some elements that should be handled with vue.
I simply want to use the vue multiselect plugin in a html file.
So I tried the following:
index.ts
import Vue from "vue"
import VueCustomElement from 'vue-custom-element'
import Autocomplete from "./vue/autocomplete.vue"
Vue.use(VueCustomElement);
Vue.customElement('auto-complete', Autocomplete);
test.html
<auto-complete
v-model="value"
:options="options"
placeholder="test"
#search-change="getData"
>
</auto-complete>
test.vue
<template>
<multiselect v-model="value" :options="options" #search-change="getData"></multiselect>
</template>
<script type="ts">
const Multiselect = require('vue-multiselect').default
export default {
components: { Multiselect },
data () {
return {
value: 'test',
options: ['list', 'of', 'options']
}
},
methods: {
getData (query) {
console.log(123)
}
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
In the output the data of the custom element is always ignored and only the parameters in the part in the .vue file is used.
How can i achieve that the parameters like placeholder or #search-change are used from the custom element?
I am also using vue-custom-elements in one of my projects.
You are passing option as props so you need to add it as a prop in your autocomplete.vue.
<template>
<multiselect v-model="value" :options="options" #search-change="getData"></multiselect>
</template>
<script type="ts">
const Multiselect = require('vue-multiselect').default
export default {
props: ['options'],
components: { Multiselect },
data () {
return {
value: 'test'
}
},
methods: {
getData (query) {
console.log('123')
}
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>