Vue: Components/templates as props - javascript

I am currently trying to learn vue and struggling with the whole component concept.
Let's say I have some components that define tabs(like browser tabs).
This component has a prop called name.
So you'd maybe use the component like so:
<tab :name="tab.display_name"></tab>
However lets say things need to be a bit more complex. Like for example, you don't just want the name to be a string, but regular HTML. Okay, so, you can use the prop in a v-html directive and use the tab component like this:
<tab :name="'<span class=\'fancy-styling\'>' + tab.display_name + '</span>'"></tab>
This one took me a while to figure out because of all the quotes. Is there a way to escape this escape hell(pun totally intended)?
How could I take it this out into its own snippet/template?
And what if we make it even more complex - say we require the prop be a vue component?
<tab :name="'<my-custom-component #click="onClick()">' + tab.display_name + '</my-custom-component>'"></tab>
The last one is invalid on a number of levels, not the least of which being the mess of quotes.
How would I accomplish this? What is the best approach?

If you are trying to inject html in props, you should really be looking at using slots.
https://v2.vuejs.org/v2/guide/components-slots.html

The whole point of using components is that you create a separation of concerns. Defining all the layered components in a single line completely defeats that purpose. Create three separate components. One container component like this.
index.vue
<template>
<tab :name="mycoolname"></tab>
</template>
<script>
import tab from tab.vue
export default {
components: {
tab
}
</script>
The name prop you can than use in that template using {{name}}. Than within the tab component, like this:
tab.vue
<template>
<h1>{{name}}</h1>
<my-custom-component #click="doStuff()"></my-custom-component>
</template>
<script>
import my-custom-component from my-custom-component.vue
export default {
props: [
'name'
],
components: {
my-custom-component
},
methods: {
doStuff () {
console.log('hooray this totally works')
}
</script>
I am using separate build files here, but the concept remains the same when you just import vue into your project. Components can contain everything, from a simple div with a <p> inside to a full page. This component system is insanely powerful to keep things organised.
Without webpack it would look something like this:
var myCustomComponent = {
template: `fancyHTML`
}
var Tab= {
components: {
'my-custom-component': myCustomComponent
},
template: `even more fancy html containing <my-custom-component>`
new Vue({
el: '#app'
components: {
'tab': Tab
}
})
}

Related

Content jump in the first milliseconds

I am creating dynamic pages using Nuxt. In the pages folder I have one file _url.vue. It contains the following code:
<template lang="pug">
div
component(
v-for="component in components"
:key="`${component.type}-${component.id}`"
:is="`the-${component.type}`"
)
</template>
<script>
// import vuex
export default {
computed: {
...mapGetters('app', {
components: 'getComponents'
})
}
}
</script>
setComponents happens at the middleware level:
export default async function ({ store }) {
await store.dispatch('app/setPage')
}
In the first milliseconds of page load, the content "jumps" as the components are rendered on the fly. How can this situation be corrected?
I'd first try to import the components manually, to see where this all comes from: the components taking some time to get injected or the layout being displayed, just to be sure.
Then, I had a discussion about it here, you may give it a look: Vue: wait to render until all components are mounted
There are several ways of handling this kind of micro-jumping explained there. You can choose your own solution. Also depends if you're using your app as universal or SPA only.
Looks like require is a way to go but some alternative are also available.

How can I add a Vue component within some HTML to another Vue component?

I have a giant, dynamic HTML string that I'm loading into a div within a Vue component. The HTML string is essentially the content from a WYSIWYG editor. Originally, I was just using v-html for this, and it was fine.
However, there are now cases where I need to replace part of the HTML string with an actual Vue component, and I'm not sure of the best way to do that.
As an example, I might have some markup in the HTML string that looks like the following:
||map:23||
And what I want to do is replace that with a Vue component like the following:
<map-component :id="23"></map-component>
I tried doing the string conversion ahead of time in Laravel and then just using v-html in the Vue component to inject the content, but that doesn't seem to load the Vue component.
I then tried using a slot for the HTML content, and that does work, but it has the nasty side effect of showing a bunch of unformatted HTML content on the screen for a second or two before Vue is able to properly render it.
So my question is: Is there another (more elegant) way to do this? I was thinking that after the Vue component loads with the HTML content, I could somehow find the, for example, ||map:23|| instances in the markup and then dynamically replace them with the correct Vue component, but if that's possible, I don't know how; I couldn't find anything in the Vue docs.
Does anyone know if this is possible? Thank you.
You can use Vue.compile to compile a template string (that can include vue components).
Then you can combine this with a component that has a render() method, to just render the template:
// this component will dynamically compile the html
// and use it as template for this component.
Vue.component("dynamic-html", {
props: ["html"],
computed: {
template() {
if(this.html)
return Vue.compile(this.html).render;
return null;
}
},
render() {
if(this.template)
return this.template();
return null;
}
});
This allows you to render arbirary template strings, which can also contain vue components:
<dynamic-html html="<some-component></some-component>">
</dynamic-html>
Additionally, you can also use this to pass down props / event handlers to components within your string:
<!-- Passing down props -->
<dynamic-html
html='<some-component :prop="$attrs.myprop"></some-component>'
:myprop="12"
></dynamic-html>
<!-- passing down events -->
<dynamic-html
html='<some-component #click="$emit('foo', $event)"></some-component>'
#foo="doSomething"
></dynamic-html>
(you need to use $attrs though to access the props, because they're not in the props definition of the dynamic-html component)
Full code example:
// this component will dynamically compile the html
// into a vue component
Vue.component("dynamic-html", {
props: ["html"],
computed: {
template() {
if(this.html)
return Vue.compile(this.html).render;
return null;
}
},
render() {
if(this.template)
return this.template();
return null;
}
});
Vue.component("red-bold-text", {
props: ["text"],
template: '<span class="red">{{text}}</span>'
});
new Vue({
el: '#root',
data: {
html: null,
myBoundVar: "this is bound from the parent component"
},
mounted() {
// get the html from somewhere...
setTimeout(() => {
this.html = `
<div>
WELCOME!
<red-bold-text text="awesome text"></red-bold-text>
<red-bold-text :text="$attrs.bound"></red-bold-text>
<button #click="$emit('buttonclick', $event)">CLICK ME</button>
</div>
`;
}, 1000);
},
methods: {
onClick(ev) {
console.log("You clicked me!");
}
}
});
.red { color: red; font-weight: bold; margin: 6px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="root">
<div>This will load dynamically:</div>
<dynamic-html :html="html" :bound="myBoundVar" #buttonclick="onClick"></dynamic-html>
</div>
Turtlefight's answer is very helpful and complete, but for anyone looking for a quick, simple answer, use the literal component component with :is as follows to inject HTML content containing Vue components into a dynamic component:
// htmlStrWithVueComponents is a large string containing HTML and Vue components.
<component
:is="{
template: `<div>${htmlStrWithVueComponents}</div>`
}"
>
</component>
Here're a couple of sources that describe this technique:
https://jannerantala.com/tutorials/vue-replace-text-with-component-with-props/
https://jsfiddle.net/Herteby/5kucj6ht/
Edit: It's worth noting that component :is is fairly limited in what you can do. You can't make very complex template strings or added mounted methods, etc.
For my particular use case, because I needed some of this more complex stuff, I ended up going with the following, which is kind of a hybrid between the simpler answer above and Turtlefight's answer:
// This code goes within the parent component's mounted method or wherever is makes sense:
Vue.component('component-name-here', {
// Can add much more complex template strings here.
template: `
<div class="someClass">
${content}
</div>
`,
// Can add lifecycle hooks, methods, computed properties, etc.
mounted: () => {
// Code here
}
});
const res = Vue.compile(`
<component-name-here>
</component-name-here>
`);
new Vue({
render: res.render,
staticRenderFns: res.staticRenderFns
}).$mount('dom-selector-for-dom-element-to-be-replaced-by-vue-component');

Use <component> that was imported in the parent

I'm building a component that manages other components.
It dynamically render components in specific places depending on the props and inputs, much like an orchestrator.
Use case
My orchestrator have the following placeholders, like a grid (p1 ... p6):
|-p1-|-p2-|-p3-|
|-p4-|-p5-|-p6-|
In a given moment, it renders the component C1 into p2 and C2 into p6:
|-p1-|-C1-|-p3-|
|-p4-|-p5-|-C2-|
In another moment, it replaces the C1 by C3:
|-p1-|-C3-|-p3-|
|-p4-|-p5-|-C2-|
The problem
Given this dynamism, I can't (as far as I know) use slots. So I'm using component tags with the :is prop in order to dynamically render the correct component. The problem is that in order for the :is to work, I must have the component defined in my orchestrator/manager component. And I would like to keep it separated for reuse, doesn't make sense to import the components there. One solution would be to globally register the components. I would like to avoid that if possible. Is there a way? I'm open to any kind of reflection-magic you may think n_n'
You can just pass the component via a prop like this:
Orchestrator.vue
<component :is="comp"/>
export default {
props: ['comp']
}
Test.vue
<orchestrator :comp="MyComponent"/>
import Orchestrator from './orchestrator.vue'
import MyComponent from './my-component.vue'
export default {
components: {
Orchestrator,
},
data() {
return {
MyComponent,
};
},
}
You should be able to use async components, loaded dynamically.
For example...
<component :is="p2Replacement"></component>
data () {
return {
p2Replacement: null
}
},
methods: {
setP2Replacement(component) {
this.p2Replacement = () => import(`path/to/${component}.vue`)
}
}
And call it like setP2Replacement('C1') or setP2Replacement('C3').
The fixed parts in the import expression are required so Webpack can include the appropriate files using a glob pattern. See https://webpack.js.org/guides/dependency-management/#require-context

VueJS Single File Component updating data

I'm really confused with how data works in single file components for VueJS. Within the file, say test.vue, as I understand, you would write out a script something like this:
export default {
name: 'Testapp',
data () {
return {
msg: 'sample message'
}
}
}
then elsewhere, say in a file called vuescript.js I would put something like following in and call it from an html file:
import Vue from 'vue'
import VApp from './test.vue'
var vueApp = new Vue({
el: '#app',
render: h => h(VApp)
})
Now how do I access that template object's data? What I'd like is to have code elsewhere that fetches data from a server and can be shared across multiple components, so I'd have a portion of data that is common across and updates from a single repository, but also I'd be able to have unshared data residing within a component for certain things like settings and other meta data.
BLUF: I'm kind of stuck after looking around a bit on how data is accessed / is handled within Vue single file components.
Data inside components should only be accessed outside of themselves in 2 ways. Either an event propagated the data upwards to a parent which can then decide if it needs to be passed to another component as a prop. Or it is stored in Vuex and accessed through getters and mutations.
Links
Component Props
Events
Vuex Example
If you want your data property to be shared by multiple components, you can use mixins.
Mixins are a flexible way to distribute reusable functionalities for Vue components. A mixin object can contain any component options. When a component uses a mixin, all options in the mixin will be “mixed” into the component’s own options.
https://v2.vuejs.org/v2/guide/mixins.html

Rendering a list based on a shared property in Vue

I'd like to render out a list of components based on whether the type property is of a certain type. For example, if resource.type === 'article' then render all of the resources with type article, etc.
My resources array looks like this
I have created a component that is basically just a view.
<template>
<div class="type-feed">
<resource-card></resource-card>
</div>
</template>
<script>
import Vue from 'vue';
import VueFire from 'vuefire';
import ResourceCard from '../components/ResourceCard'
var resourcesRef = firebase.database().ref('resources');
// explicit installation required in module environments
Vue.use(VueFire);
export default {
name: 'type-view',
components: {
ResourceCard
},
data () {
return {
}
},
firebase: {
resources: resourcesRef
}
}
</script>
<style>
</style>
The <resource-card> component should take in the list and render a specific information on an individual resource object. Something like the following
<resource-card v-for="(resource, index) in resources" :resource="resource>
My question is, what is the best way to render a list by resources.type?
If <resource-card> is not used anywhere else, then just pass the resource to it and let it decide what to render according to resource.type should be enough.
If you could need a article card later elsewhere, or you want a more elegant design, then you may define a component for each resource type, and in the v-for, you can use dynamic components to render different components in one loop.

Categories

Resources