VueJS: Using x-template for a sub-component inside a component - javascript

I am not correctly understanding the usage pattern for using x-template for a subcomponent inside a vuejs component.
So, I have a component called CategoryNav.vue which has a template. Within that to display a list, we have used an x-template. But I render my page, it doesn't recognize this component created using the x-template. I think i am using it incorrectly. Any help is appreciated. Here is my main component code.
CategoryNav.vue
<template>
<div class="">
<menu-list :items="treeList"></menu-list>
</div>
</template>
<script type="text/x-template" id="menu-list-template">
<ul v-if="items.length">
<li v-for="item of items">
<a :href="item.value">{{ item.label }}{{ item.id }}</a>
<menu-list :items="item.children"></menu-list>
</li>
</ul>
</script>
<script>
const MenuList = {
name: 'menu-list',
template: '#menu-list-template',
props: ['items']
}
export default {
name: 'category-nav',
components: {
MenuList
},
computed: {
list () {
return this.$store.state.topics
},
treeList () {
const items = this.list.map(item => Object.assign({}, item, { children: [] }))
const byValue = new Map(items.map(item => [item.value, item]))
const topLevel = []
for (const item of items) {
const parent = byValue.get(item.parent)
if (parent) {
parent.children.push(item)
} else {
topLevel.push(item)
}
}
return topLevel
}
}
}
</script>

This won't work. <script type="text/x-template" id="menu-list-template"> needs to exist in the DOM somewhere for Vue to find it, and since it is outside of the <template> section, vue-loader is going to treat it as a custom block (which will be ignored by default).
A single *.vue file is meant to contain one component only. I don't recommend mixing x-templates with vue-loader anyway. You should put every component into a *.vue file so that it gets precompiled, that way you don't need to bundle the vue compiler in your production build.
You can either:
(Recommended) Extract the sub component into its own *.vue file and import it into the CategoryNav.vue module.
You can define the sub component completely within CategoryNav.vue <script> section, but it cannot have a compiled template. You'll have to specify the render function for it (messy).
Same as #2, except you can specify the template as a string, but you will need to ship the Vue compiler in your production build. If you want to use an x-template instead of a string, then you need to make sure that the x-template <script> is in the DOM.
See this for an explanation of the Vue build files.

You didn't declare component name 'menu-list', let's try this:
export default {
name: 'category-nav',
components: {
'menu-list': MenuList
},
....

Related

Pass data attribute to vue 3 root instance [duplicate]

I am terribly new to Vue, so forgive me if my terminology is off. I have a .NET Core MVC project with small, separate vue pages. On my current page, I return a view from the controller that just has:
#model long;
<div id="faq-category" v-bind:faqCategoryId="#Model"></div>
#section Scripts {
<script src="~/scripts/js/faqCategory.js"></script>
}
Where I send in the id of the item this page will go grab and create the edit form for. faqCategory.js is the compiled vue app. I need to pass in the long parameter to the vue app on initialization, so it can go fetch the full object. I mount it with a main.ts like:
import { createApp } from 'vue'
import FaqCategoryPage from './FaqCategoryPage.vue'
createApp(FaqCategoryPage)
.mount('#faq-category');
How can I get my faqCategoryId into my vue app to kick off the initialization and load the object? My v-bind attempt seems to not work - I have a #Prop(Number) readonly faqCategoryId: number = 0; on the vue component, but it is always 0.
My FaqCategoryPAge.vue script is simply:
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Prop } from 'vue-property-decorator'
import Card from "#/Card.vue";
import axios from "axios";
import FaqCategory from "../shared/FaqCategory";
#Options({
components: {
Card,
},
})
export default class FaqCategoryPage extends Vue {
#Prop(Number) readonly faqCategoryId: number = 0;
mounted() {
console.log(this.faqCategoryId);
}
}
</script>
It seems passing props to root instance vie attributes placed on element the app is mounting on is not supported
You can solve it using data- attributes easily
Vue 2
const mountEl = document.querySelector("#app");
new Vue({
propsData: { ...mountEl.dataset },
props: ["message"]
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Vue 3
const mountEl = document.querySelector("#app");
Vue.createApp({
props: ["message"]
}, { ...mountEl.dataset }).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Biggest disadvantage of this is that everything taken from data- attributes is a string so if your component expects something else (Number, Boolean etc) you need to make conversion yourself.
One more option of course is pushing your component one level down. As long as you use v-bind (:counter), proper JS type is passed into the component:
Vue.createApp({
components: {
MyComponent: {
props: {
message: String,
counter: Number
},
template: '<div> {{ message }} (counter: {{ counter }}) </div>'
}
},
}).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app">
<my-component :message="'Hello from HTML'" :counter="10" />
</div>
Just an idea (not a real problem)
Not really sure but it can be a problem with Props casing
HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you're using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents
Try to change your MVC view into this:
<div id="faq-category" v-bind:faq-category-id="#Model"></div>
Further to Michal Levý's answer regarding Vue 3, you can also implement that pattern with a Single File Component:
app.html
<div id="app" data-message="My Message"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const mountEl = document.querySelector("#app");
Vue.createApp(MyComponent, { ...mountEl.dataset }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
Or you could even grab data from anywhere on the parent HTML page, eg:
app.html
<h1>My Message</h1>
<div id="app"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const message = document.querySelector('h1').innerText;
Vue.createApp(MyComponent, { message }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
To answer TheStoryCoder's question: you would need to use a data prop. My answers above demonstrate how to pass a value from the parent DOM to the Vue app when it is mounted. If you wanted to then change the value of message after it was mounted, you would need to do something like this (I've called the data prop myMessage for clarity, but you could also just use the same prop name message):
<template>
{{ myMessage }}
<button #click="myMessage = 'foo'">Foo me</button>
</template>
<script>
export default {
props: {
message: String
},
data() {
return {
myMessage: this.message
}
}
};
</script>
So I'm not at all familiar with .NET and what model does, but Vue will treat the DOM element as a placeholder only and it does not extend to it the same functionality as the components within the app have.
so v-bind is not going to work, even without the value being reactive, the option is not there to do it.
you could try a hack to access the value and assign to a data such as...
const app = Vue.createApp({
data(){
return {
faqCategoryId: null
}
},
mounted() {
const props = ["faqCategoryId"]
const el = this.$el.parentElement;
props.forEach((key) => {
const val = el.getAttribute(key);
if(val !== null) this[key] = (val);
})
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="12">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
where you get the value from the html dom element, and assign to a data. The reason I'm suggesting data instead of props is that props are setup to be write only, so you wouldn't be able to override them, so instead I've used a variable props to define the props to look for in the dom element.
Another option
is to use inject/provide
it's easier to just use js to provide the variable, but assuming you want to use this in an mvc framework, so that it is managed through the view only. In addition, you can make it simpler by picking the exact attributes you want to pass to the application, but this provides a better "framework" for reuse.
const mount = ($el) => {
const app = Vue.createApp({
inject: {
faqCategoryId: {
default: 'optional'
},
},
})
const el = document.querySelector($el)
Object.keys(app._component.inject).forEach(key => {
if (el.getAttribute(key) !== null) {
app.provide(key, el.getAttribute(key))
}
})
app.mount('#app')
}
mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="66">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
As i tried in the following example
https://codepen.io/boussadjra/pen/vYGvXvq
you could do :
mounted() {
console.log(this.$el.parentElement.getAttribute("faqCategoryId"));
}
All other answers might be valid, but for Vue 3 the simple way is here:
import {createApp} from 'vue'
import rootComponent from './app.vue'
let rootProps = {};
createApp(rootComponent, rootProps)
.mount('#somewhere')

Vue.js on render populate content dynamically via vue.router params

AS title sates, I don't so much need a solution but I don't understand why I'm getting the undesired result;
running v2 vue.js
I have a vue component in a single component file.
Basically the vue should render data (currently being imported from "excerciseModules" this is in JSON format).
IT's dynamic so based on the url path it determines what to pull out of the json and then load it in the page, but the rendering is being done prior to this, and I'm unsure why. I've created other views that conceptually do the samething and they work fine. I dont understand why this is different.
I chose the way so I didn't have to create a ton of routes but could handle the logic in one view component (this one below).
Quesiton is why is the data loading empty (it's loading using the empty "TrainingModules" on first load, and thereafter it loads "old" data.
Example url path is "https...../module1" = page loads empty
NEXT
url path is "https..../module 2" = page loads module 1
NEXT
url path is "https..../module 1" = page loads module 2
//My route
{
path: '/excercises/:type',
name: 'excercises',
props: {
},
component: () => import( /* webpackChunkName: "about" */ '../views/training/Excercises.vue')
}
<template>
<div class="relatedTraining">
<div class="white section">
<div class="row">
<div class="col s12 l3" v-for="(item, index) in trainingModules" :key="index">
<div class="card">
<div class="card-content">
<span class="card-title"> {{ item.title }}</span>
<p>{{ item.excercise }}</p>
</div>
<div class="card-action">
<router-link class="" to="/Grip">Start</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
console.log('script');
let trainingModules; //when initialized this is empty, but I would expect it to not be when the vue is rendered due to the beforeMount() in the component options. What gives?
/* eslint-disable */
let init = (params) => {
console.log('init');
console.log(trainingModules);
trainingModules = excerciseModules[params.type];
//return trainingModules
}
import { getRandom, randomImage } from '../../js/functions';
import { excerciseModules } from '../excercises/excercises_content.js'; //placeholder for JSON
export default {
name: 'excercises',
components: {
},
props: {
},
methods: {
getRandom,
randomImage,
init
},
data() {
return {
trainingModules,
}
},
beforeMount(){
console.log('before mount');
init(this.$route.params);
},
updated(){
console.log('updated');
},
mounted() {
console.log('mounted');
//console.log(trainingModules);
}
}
</script>
I can't tell you why your code is not working because it is an incomplete example but I can walk you through a minimal working example that does what you are trying to accomplish.
The first thing you want to do, is to ensure your vue-router is configured correctly.
export default new Router({
mode: "history",
routes: [
{
path: "/",
component: Hello
},
{
path: "/dynamic/:type",
component: DynamicParam,
props: true
}
]
});
Here I have a route configured that has a dynamic route matching with a parameter, often called a slug, with the name type. By using the : before the slug in the path, I tell vue-router that I want it to be a route parameter. I also set props: true because that enables the slug value to be provided to my DynamicParam component as a prop. This is very convenient.
My DynamicParam component looks like this:
<template>
<div>
<ul>
<li v-for="t in things" :key="t">{{ t }}</li>
</ul>
</div>
</template>
<script>
const collectionOfThings = {
a: ["a1", "a2", "a3"],
b: ["b1", "b2"],
c: [],
};
export default {
props: ["type"],
data() {
return {
things: [],
};
},
watch: {
type: {
handler(t) {
this.things = collectionOfThings[t];
},
immediate: true,
},
},
};
</script>
As you can see, I have a prop that matches the name of the slug available on this component. Whenever the 'slug' in the url changes, so will my prop. In order to react to those changes, I setup a watcher to call some bit of code. This is where you can make your fetch/axios/xhr call to get real data. But since you are temporarily loading data from a JSON file, I'm doing something similar to you here. I assign this data to a data value on the component whenever the watcher detects a change (or the first time because I have immediate: true set.
I created a codesandbox with a working demo of this: https://codesandbox.io/s/vue-routing-example-forked-zesye
PS: You'll find people are more receptive and eager to help when a minimal example question is created to isolate the problematic code. You can read more about that here: https://stackoverflow.com/help/minimal-reproducible-example

Get slot data as a variable in Vue?

I have a component (prism-editor) that only takes code from v-model="code". This means, the code has to be sent to the component through code:
Code.vue
<template>
<prism-editor class="my-editor" v-model="code"
:highlight="highlighter" :line-numbers="numbers"/>
</template>
<script>
import { PrismEditor } from 'vue-prism-editor';
export default {
components: {
PrismEditor,
},
data: () => ({
code: this.$slots,
numbers: true
}),
}
</script>
I would like to bind this from a parent component named Code from a slot:
Page.vue
<template>
<code language="c">
int main() {
printf('Hello World!');
}
</code>
<template>
<script>
import Code from 'code.vue'
export default {
components: {
'code': Code
}
}
</script>
In my Code component, I have to find a way to get and pass the slot data directly to the code variable to be sent to the v-model='code'. Unfortunately the following doesn't work because I don't know how to get the data from the parent slot:
data: () => ({
code: this.$slots // Which obviously doesn't work...
})
Said differently, I just want to get all the raw content that was sent inside the code tag:
<code>all this content...</code>`
Is this possible?
.
├── Code.vue
└── Page.vue
Great question, in order to solve that you would have to go a layer below Vuejs, and use the property textContent from the DOM API [read more here]
With this property you can access the content inside of a DOM element, so in your case it would be something like:
/*
* Template
*/
<div ref="mySlot">
<slot></slot>
</div>
/*
* Javascript
*/
this.$refs.mySlot.textContent;
I've setup a nice example for you in Codesandbox:
https://codesandbox.io/s/gallant-resonance-7unn2?file=/src/components/Code.vue
For future challenges:
Always try to see if you can solve it with pure Javascript. Happy coding mate;

dynamically render vue template

I have vue template data as string. For example,
String s = "<div>{{myData}}</div>"
And now I want to render in my already defined vue component.
<template>
<div>
HERE I NEED TO PLACE THE STRING
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data: {
myData: "IRONMAN"
},
}
</script>
Now I want the output as IRONMAN
How can i achieve this? Pleas help.
Thanks
You can have a single file component and do this - I have one called Dynamic.vue which accepts a HTML string - I use this to allow the users to generate their own templates and the bindings all match up properly, something like:
<script>
export default {
data () {
return {
someVar: 'Test'
}
},
props: {
templateHtml: {
templateHtml: true,
type: String
}
},
created () {
this.$options.template = this.templateHtml
}
}
</script>
If you were to call it like:
this.htmlData = '<div>Hello - {{{someVar}}</div>'
....
<my-dynamic-component :template-html="htmlData" />`
You would see the output
Hello Test
You would then omit the <template> part in the SFC.
Note: In order for this to work, you must also have the Vue Compiler included in your project (as this handles compiling the SFC into render functions which Vue uses to display data).
This link: https://v2.vuejs.org/v2/guide/installation.html#CLI can give more information about including the Vue Compiler.

Equivalent of a React container and presentational component in Vue.js

I would like to separate the business logic and the template of a component.
In React I would use the Container/Presentation pattern.
const Container = (props) => <Presentational ...props/>
But what is the equivalent with vue.js?
Say I have this all in one component (did not test this one, it just for example) :
<template>
<div id="app">
<div v-for="user in users">
{{user.name}}
</div>
</div>
</template>
<script>
Vue.component({
el: '#app',
props: {
filter: "foo"
},
data: {
users: [],
},
ready: function () {
this.$http.get('/path/to/end-point?filter='+this.props.filter)
.then(function (response) {
this.users = response.data
})
}
})
</script>
How I could extract a container with just the fetch logic?
I really don't see a need for a container component. Abstract your fetch code out into a vuex action and bind your store state within the component using computed properties.
You can extend any Vue component, which will allow you to override any methods from the parent. So, you can create a base component and then extend that:
const Base = Vue.component('base-comp', {
template: "#base",
data() {
return {
name: 'foo'
}
}
});
const Child = Base.extend({
data() {
return {
name: 'bar'
}
}
});
Vue.component('child-comp', Child)
You can check out the JSFiddle here: https://jsfiddle.net/tdgxdhz9/
If you are using single file components, then it's simply a case of importing the base component and extending it, which keeps the original template in tact:
import Base from './BaseComponent.vue'
Base.extend({
// Javascript code here
})
You can use vuex-connect to create container components like in React. Here an example of project : https://github.com/pocka/vue-container-component-example
What you are looking for is Vue Mixins.
You can write a mixins file which contains your business logic and import it into your Vue components.
Link:- https://v2.vuejs.org/v2/guide/mixins.html

Categories

Resources