In the simplified example below I demonstrate my problem:
I have a for-loop that asynchronously updates myItems.
I want to be able and update accordingly selectableItems by using this.$el.querySelector('selectable-item').
<template>
<div>
<p>selectableItems: {{selectableItems}}</p>
<div v-for="item in myItems" class="selectable-item">item</div>
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
myItems: [],
selectableItems: [],
}
},
created(){
// Populate myItems with a delay
self = this
setTimeout(function() {
self.myItems = [1, 2, 3]
}, 1000);
},
mounted(){
// Fetch some of myItems based on a class
this.selectableItems = this.$el.querySelectorAll('.selectable-item')
},
}
</script>
<style scoped lang="scss">
</style>
I've tried many different things I've found online; TickNext, computed, updated, etc. I think I'm doing something fundamentally wrong. But it is important for my case to be able and select DOM elements by a class.
Any help is deeply appreciated.
Updated: More context
Some people asked me for the bigger picture so I give a bit more info here.
Currently I have a big Vue component where the user is able to select elements. I am trying to factor out all this user interaction into a mixin so I can re-use it in other places of my code.
To make re-usability easy I need to be able and simply add a class selectable to any HTML tag in the template. That's the interface and then the mixin does all the magic and populates selectedElements depending on user interaction.
That's why it is important to avoid refs, etc. since then too much logic leaks everywhere and beats the purpose of making the mixin re-usable. Unless I'm missing something.
OK, after trying many different things I've managed to solve this by using a non-reactive intermediate variable. This means that I can't use the variable in a template but that is fine.
export default {
..
// NOT reactive so you can't just use it in your templates.
_selectableItems: [],
updated(){
self._selectableItems = self.$el.querySelectorAll('.selectable-item')
},
..
}
Related
So I’m building a Nuxt app for working with docs (in a broad sense), and it will have a menu, which I will obviously make a component. The menu will be home to lots of actions on the doc itself, such as opening/saving files, editing, etc. etc.
I know the standard way to pass info from a component to its parent (the doc vm in this case) is via messages, but it feels like a bit of an overkill, what with the syntax (emit handlers just don’t feel natural to me in this case) and whatnot.
For this reason I was wondering why can’t I just pass the parent vm as a prop to the menu component? It will contain all kinds of methods, and I will be able to easily invoke them via the menu. Something like:
Parent (Document.vue):
<template>
<main-menu :document='vm'/>
</template>
<script>
import MainMenu from '~/components/MainMenu.vue'
export default {
data(): {
return {
vm: this,
//...
}
},
methods: {
save() {
//...
}
}
//...
</script>
Menu component (MainMenu.vue):
<template>
<button #click='document.save()'>Save document</button>
</template>
<script>
export default {
props = ['document']
}
</script>
The question: Is there something intrinsically bad in this approach?
(I imagine this could be problematic if the app architecture could change, but it’s hard to imagine that I would for some reason need a menu without an underlying document.)
IF your Menu is always the child of the component, then you don't have to pass your parent. It is already held in a Vue variable called this.$parent.
I made a little sandbox to give you an example.
The parent has a function, for example:
/// PARENT
export default {
name: "App",
components: {
HelloWorld,
},
methods: {
iExist(add) {
console.log("I am in parent" + add);
},
},
};
Then you can call it from child with this.$parent.iExist('something').
Since this.$parent is not defined when the template is being evaluated, we have to make a method in the child as well, to call(super) the corresponding function on it's parent.
/// CHILD
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<button #click="iExist(', but was called from child')">Click Me</button>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
},
methods: {
iExist(add) {
this.$parent.iExist(add);
},
},
};
</script>
The question: Is there something intrinsically bad in this approach?
(I imagine this could be problematic if the app architecture could change, but it’s hard to imagine that I would for some reason need a menu without an underlying document.)
Yes, this is bad design. Parents can be aware of children, children shouldn't be aware of parents. A child could be tested in isolation, or be nested inside wrapper component that doesn't have this method.
As another answer suggests, a way to access a parent is to use $parent property. This part was borrowed in Vue from AngularJS 1.x, accessing it was considered a bad practice even then.
This is generally achieved by providing a callback from a parent that does exactly a desired thing, without allowing to access the whole instance and break the encapsulation. It's unnecessary to explicitly define callback function in Vue because this is naturally provided by Vue template syntax:
In a parent:
<child #save="save()">
In a child:
<button #click="$emit('save')">
In case of deeply nested components the event can be passed through them to a parent.
I have a Component where each instance needs some data being passed onto it, that Component is used multiple times and all instances of the component should receive the same data/prop.
<my-component :someProp="someValue"></my-component>
<my-component :someProp="someValue"></my-component>
<my-component :someProp="someValue"></my-component>
<!-- ...and lots more... -->
This is my current method:
Which gets kind of redundant, how can I pre-populate this someProp for ALL instances of my component?
I tried Vue.extend() but cannot figure out what syntax it expects, the documentation is not clear enough on that part.
This is how I imagined it to work:
// App.vue
import MyComponent from './components/MyComponent'
const PreConfiguredComponent = Vue.extend(MyComponent, {
props: {
someProp: "someValue"
}
})
export default {
name: 'app',
data: () => ({}),
components: {
MyComponent: PreConfiguredComponent
}
}
You get the idea, I don't know how to express it better. Doesn't have to be done with props but I don't know other methods to pass data along.
There are quite a lot of different ways to approach this. I doubt this will be anything like an exhaustive list but I can give some sense of the possibilities.
1. Store the value globally
The obvious choice here is the Vuex store. Just put the relevant data in the store and then the components can grab what they need.
However, a global store doesn't have to be Vuex. If you have no other reason to introduce Vuex then you might prefer something more ad hoc.
An alternative to the store is to hold the data on $root, an approach described in the official documentation:
https://v2.vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
A further alternative is just to have a .js file that can hold the data as a singleton. There are various ways to do it but in its simplest form it might be something like this:
export default {
myData: null
}
You'd then import that object and read/write the value of myData as required. It won't necessarily be reactive but I'm assuming that the initial value would be set at the start, before the application is created, and wouldn't change after that.
2. Store the value on the Vue prototype
This is quite similar to the above but I think it warrants a separate mention. It'd look something like this:
Vue.prototype.$myComponentData = 'someValue'
Then within any component you could access the value via the property $myComponentData.
Documentation: https://v2.vuejs.org/v2/cookbook/adding-instance-properties.html
3. Provide/Inject
The provide/inject mechanism is one of the lesser known Vue features but it provides an alternative to using props to pass data down to child components. It has various pros and cons and typically you'd try to use props instead.
https://v2.vuejs.org/v2/api/#provide-inject
In short, it allows a component to provide a named value to all of its descendants without needing to explicitly pass it to each one individually. The descendant components can then choose which named properties they would like to have injected.
You wouldn't be able to use provide/inject to pass different values to descendant components but in your case the value is the same so it should work.
If you think this approach might be for you then I suggest some further reading:
https://blog.logrocket.com/how-to-make-provide-inject-reactive/
4. Refactor to remove the duplication
While this is unlikely to be the solution you go with I do think it is worth mentioning.
The starting premise for the question seems to be that passing the prop explicitly is a form of duplication. Extra noise and extra effort with the potential to make mistakes.
Potentially we can remove that duplication while still passing the prop. The key here is to refactor the template so that the child component only features once.
Obviously that would need to be within a loop so that we still get all the desired components. That loop would need a suitable data structure to drive it so that all the right components get created.
I assume the template in the question is a simplification. If you really do have several instances consecutively then refactoring to use a v-for should be pretty trivial. If, as seems more likely, the components are nested in various places within the layout then it can get unwieldy trying to encode that in data structures just to avoid a bit of template duplication.
Hopefully the idea is clear but if not you could give this a read:
https://michaelnthiessen.com/reducing-redundant-repetition
5. Dynamic components
This is what's alluded to in the question. There are various ways to do it but the core of the idea is that we change our component definitions at runtime. That doesn't necessarily mean creating a new component, it could equally mean prodding something into an existing component definition. We've already seen a variation of this idea with the Vue.prototype approach mentioned earlier.
In theory it could be done with the default value of a prop but it seems unnecessary to use a prop unless that prop is going to be set from the outside in the usual way in some cases.
We could set it as a property using data but personally I would be tempted to use a variation of the Vue.prototype trick to add the property to the component's own prototype:
MyComponent = Vue.extend({
template: `<div>{{ value }}</div>`
})
MyComponent.prototype.value = 'some value'
new Vue({
el: '#app',
components: {
MyComponent
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<my-component></my-component>
</div>
Using the prototype is relatively cheap from a performance perspective. There are potential problems with reactivity but they would only apply if the value can change, which it can't in this case.
Just to illustrate the prop and data approaches explicitly:
MyComponent = {
template: `<div>{{ value }}</div>`
}
PreConfiguredComponent = Vue.extend({
extends: MyComponent,
props: {
value: {
default: 'some prop value'
}
}
})
new Vue({
el: '#app',
components: {
MyComponent: PreConfiguredComponent
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<my-component></my-component>
</div>
and:
MyComponent = {
template: `<div>{{ value }}</div>`
}
PreConfiguredComponent = Vue.extend({
extends: MyComponent,
data () {
return {
value: 'some data value'
}
}
})
new Vue({
el: '#app',
components: {
MyComponent: PreConfiguredComponent
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<my-component></my-component>
</div>
You could equally try to tweak the imported MyComponent directly rather than extending it.
TL;TR
How do I:
getMyComponent(selectedMyComponentID).complexOperation()
To me this sounds like such a trivial and useful thing, e.g. from a pulldown menu.
A little more detail
Assume I'm making an editor of some kind (think todo-list or something). The GUI has the concept of a "selected" element. (In our real case it is the currently visible bootstrap nav-tab), and I want to have pull-down menus with menu-items that perform different operations on the selected element.
Assuming I have the id of the "selected" component, getting a reference to the MyComponent that has a complexOperation() method corresponding to the id is surprisingly difficult.
Perhaps that is because I'm not doing this "the Vue way".
I see these ways to accomplish complexOperationOnSelectedMyComponent():
use refs - seems messy and ugly
refactor the complexOperation() out of MyComponent and into a new MyData, so the business logic on the data is used by oth App.vue and MyComponent.vue. Now the parent is changing data and therefore props - sounds vue-ish. But that leads to lots of boilerplate since every operation in every component now has two versions. I'm not a fan of boilerplate and duplication...
use vuex? I think I'm not there yet...
Use an "event bus" Vue instance and $emit events from parent to child. Seems overkill. And is messy and has boilerplate.
Am I missing something? Isn't this pretty standard stuff?
Details
For simplicity, we'll say that there is a template:
<template>
<div id="app">
<div v-for="elem in mydata" :key="elem.id" #click="setSelected(elem)">
<MyComponent :value="elem"/>
</div>
<button #click="complexOperationOnSelectedComponent">
Complex operation on Selected Component
</button>
</div>
</template>
and a data structure where the first one is initially selected:
data() {
return {
mydata: [
{ id: 0, foo: "bar", selected: true },
{ id: 1, foo: "baz", selected: false },
{ id: 2, foo: "world", selected: false }
]
};
}
(Complete codesandbox)
So there is a "Complex operation on Selected Component" button. But what should I put in the complexOperationOnSelectedComponent method?
The codesandbox above also has equivalent buttons inside each MyComponent. They simply call a complexOperation() method in the MyComponent definition.
I'm thinking that whether the button happens to be inside or outside the component is a minor detail. Get a reference to the MyComponent for the selected id and call selectedComponent.complexOperation() in the menu item's #click handler.
In our real scenario, the user selects the "component" by clicking on a nav-bar, (not on the MyComponent instance), so what we have is a id (mydata[n].id or 0, 1 or 2 above).
Using ref-s
What I could do was put ref="components" in the <MyComponents> definition. Because it is in a v-for loop, this.$refs.components will then be an array of MyComponents. Find the one with the right id and use it.
Because there is no guarantee about the order of in this.$refs.components I'd have to search the array for the selectedMyComponentID every time, but hey...
Is this really the best solution?
You typically want to use $refs if you need access to the DOM directly to get to elements that are not connected/controlled to any Vue instance properties.
You can store which element is selected in data and target that specific element.
Putting your method in the parent or the child? It depends, do you need the logic in other places? In this case, I think it makes sense in the child because you want to be able to perform actions there as well.
Use an "event bus" Vue instance and $emit events from parent to child. Seems overkill.
Props go from parent to child. Events are emitted from child to parent.
Vuex and event busses are really helpful in larger applications but really not needed in this case. You should, however, emit changes that you want to make to your props and not edit them directly like you are doing now in MyComponent.
I refactored your code a bit, I do want to repeat that modifying the values of props directly is bad practice: https://codesandbox.io/s/button-onclick-on-selected-child-lf37c?fontsize=14
<template>
<div id="app">
<div v-for="elem in mydata" :key="elem.id" #click="selectedElem = mydata[elem.id]">
<MyComponent :value="elem" :reverse="elem.reverse"/>
</div>
<button #click="reverseSelectedFoo">Reverse Selected Foo</button>
</div>
</template>
<script>
import MyComponent from "./components/MyComponent";
export default {
name: "App",
data() {
// Ideally this data is read from an API somewhere
return {
mydata: [
{ id: 0, foo: "bar", reverse: false },
{ id: 1, foo: "baz", reverse: false },
{ id: 2, foo: "world", reverse: false }
],
selectedElem: null
};
},
methods: {
reverseSelectedFoo() {
this.selectedElem.reverse = !this.selectedElem.reverse;
}
},
components: {
MyComponent
}
};
</script>
So, I'm attempting to create a Gutenburg style blog, I'm working on a block creation method. I should caveat - this is entirely my method, if this is the wrong method - great, let me know, but please let me know the correct way! :)
Back to the question. In my project I have the following component.
baseComponent.vue:
<template>
<component v-for="contentBlock in contentBlocks" v-bind:is="contentBlocks.blockComponent" v-bind:key="contentBlock.id" transition="fade" transition-mode="out-in"></component>
</template>
<script>
import CodeBlockComponent from './codeBlockComponent';
export default {
name: 'BaseComponent',
components: {
CodeBlockComponent <!-- Corresponds to the name given in './codeBlockComponent'
},
data: () => ({
contentBlocks: []
})
watch: {
contentBlocks () {
console.log(this.contentBlocks)
}
},
methods: {
addCodeBlock () {
console.log('Code Block Added!')
this.contentBlocks.push({ 'id': this.contentBlocks.length + 1, 'blockType': 'code', 'blockComponent': 'CodeBlockComponent', 'content': '' })
},
addQuoteBlock () {
console.log('Quote Block Added!')
this.contentBlocks.push({ 'id': this.contentBlocks.length + 1, 'blockType': 'quote', 'content': '' })
}
}
}
</script>
N.B. Where above I have stripped the complexity from my template.
Within this same baseComponent I also have buttons which add blocks to the contentBlocks array, where my watch method is definitely finding blocks when added to this array (see below for a screenshot of the console output):
So, everything seems to be going ok - I'm now ready to add block Components. I add the first one, and I receive the following error in the console:
vue.runtime.esm.js?2b0e:587 [Vue warn]: Property or method "CodeBlockComponent" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
I took one look at the documentations where it advised to head to, which was here: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties
An example of the CodeBlockComponent.vue:
<template>
<div>
<p>HELLO WORLD FROM THE CODE BLOCK!!</p>
</div>
</template>
<script>
export default {
name: 'CodeBlockComponent',
data: () => ({
}),
computed: {
},
watch: {
},
methods: {
}
}
</script>
I took one look, and I'll be 100% honest - I don't quite understand what it is telling me to do...I feel like I have declared a reactive property? Any advise or pointers anyone can give me would be greatly appreciated!
I'm assuming I can't simply import CodeBlockComponent from './codeBlockComponent'; for this sort of dynamic rendering of components? (But, I really don't know at this point)...
I think what you described should work just fine in case if you've imported and declared in
components: { ... }
all possible names which could be found in each contentBlock.blockComponent. Notice those components must have their name: <String> exactly the same as in your contentBlock.blockComponent. I can't see in your example this prop for item from addQuoteBlock by the way.
You also provided link on documentation, but it's about props, which works just fine in your example. Recheck section about dynamic components, maybe it will help: https://v2.vuejs.org/v2/guide/components.html#Dynamic-Components (notice links on fiddles)
One more thing to check: does your component in baseComponent.vue wrapped with some ? Component must have single root element. Component with v-for probably won't go.
I'd like to insert new vuejs components on the fly, at arbitrary points within a block of not-necessarily-predefined HTML.
Here's a slightly contrived example that demonstrates the sort of thing I'm trying to do:
Vue.component('child', {
// pretend I do something useful
template: '<span>--><slot></slot><--</span>'
})
Vue.component('parent', {
data() {
return {
input: 'lorem',
text: '<p>Lorem ipsum dolor sit amet.</p><p><i>Lorem ipsum!</i></p>'
}
},
template: `<div>
Search: <input type='text' v-model="input"><br>
<hr>
This inserts the child component but doesn't render it
or the HTML:
<div>{{output}}</div>
<hr>
This renders the HTML but of course strips out the child component:
<div v-html="output"></div>
<hr>
(This is the child component, just to show that it's usable here:
<child>hello</child>)
<hr>
This is the goal: it renders both the input html
and the inserted child components:
TODO ¯\_(ツ)_/¯
</div>`,
computed: {
output() {
/* This is the wrong approach; what do I replace it with? */
var out = this.text;
if (this.input) {
this.input = this.input.replace(/[^a-zA-Z\s]/g,'');
var regex = new RegExp(this.input, "gi");
out = out.replace(regex, '<child><b>' + this.input + '</b></child>');
}
return out;
}
}
});
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script>
<div id="app">
<parent></parent>
</div>
In the above snippet, assume data.text is sanitized HTML. <child> is some sub-component that does something useful, which I want to wrap around chunks of data.text that aren't known ahead of time. (input is just for demo here. This MCVE doesn't really resemble the code I'm building, it's just an example that shows the sort of situation I'm stuck on.)
So: how would I change either the output function or the parent component's template, such that both the HTML from input and the inserted <child> templates are rendered properly?
What I've tried
In Vue 1, the answer to this would be a straightforward $compile. I'm using vuejs2 which removed $compile (out of justifiable concern that it made it too easy to naively introduce XSS vulnerabilities.)
v-html sanitizes what you feed it, which strips the child component out. Obviously this is not the way to do this. (That page suggests using partials instead, but I'm not sure how that could be applied to this situation; in any case partials have also been removed from vue2.)
I've tried passing the results of output() into another component which would then use it as its template. This seems like a promising approach, but I can't figure out how to change that secondary component's template. template only accepts a string, not a function like many of the other component properties, so I can't pass the template html in, say, a prop. Something like rewriting this.template inside beforeMount() or bind() would have been nice, but no joy there either. Is there some other way to replace a component's template string before it's mounted?
Unlike template, I can pass data to a component's render() function... but then I'm still stuck having to parse that html string into nested createElement functions. Which is exactly what Vue is doing internally in the first place; is there some way to hook into that here short of reinventing it myself?
Vue.component('foo', {
props: ['myInput'],
render(createElement) {
console.log(this.myInput); // this works...
// ...but how to parse the html in this.myInput into a usable render function?
// return createElement('div', this.myInput);
},
})
I wasn't able to cheat my around this with inline-template, either: <foo inline-template>{{$parent.output}}</foo> does exactly the same thing as a plain old {{output}}. In retrospect that should have been obvious, but it was worth a shot.
Maybe constructing an async component on the fly is the answer? This could clearly generate a component with an arbitrary template, but how would I reasonably call that from the parent component, and feed output to the constructor? (It would need to be reusable with different input, with multiple instances potentially visible simultaneously; no globals or singletons.)
I've even considered ridiculous stuff like having output() split the input into an array at the points where it would have inserted <child>, and then doing something like this in the main template:
...
<template v-for="chunk in output">
<span v-html="chunk"></span>
<child>...</child>
</template>
....
That would be doable, if laborious -- I'd have to split out what goes in the child's slot into a separate array too and get it by index during the v-for, but that could be done... if input were plain text instead of HTML. In splitting HTML I'll often wind up with unbalanced tags in each chunk, which can mess up the formatting when v-html rebalances it for me. And anyway this whole strategy feels like a bad hack; there must be a better way.
Maybe I just drop the whole input into a v-html and then (somehow) insert the child components at the proper positions through after-the-fact DOM manipulation? I haven't explored this option too deeply because it, too, feels like a hack, and the reverse of the data-driven strategy, but maybe it's a way to go if all else fails?
A couple of pre-emptive disclaimers
I'm very well aware of the XSS risks involved in $compile-like operations. Please be assured that none of what I'm doing involves unsanitized user input in any way; the user isn't inserting arbitrary component code, instead a component needs to insert child components at user-defined positions.
I'm reasonably confident that this is not an XY problem, that I really do need to insert components on the fly. (I hope it's obvious from the number of failed attempts and blind alleys I've run down that I've put more than a little thought into this one!) That said, if there's a different approach that leads to similar results, I'm all ears. The salient point is that I know which component I need to add, but I can't know ahead of time where to add it; that decision happens at run time.
If it's relevant, in real life I'm using the single-file component structure from vue-cli webpack template, not Vue.component() as in the samples above. Answers that don't stray too far from that structure are preferred, though anything that works will work.
Progress!
#BertEvans points out in comments that Vue.compile() is a thing that exists, which is an I-can't-believe-I-missed-that if ever there was one.
But I'm still having trouble using it without resorting to global variables as in that documentation. This renders, but hardcodes the template in a global:
var precompiled = Vue.compile('<span><child>test</child></span>');
Vue.component('test', {
render: precompiled.render,
staticRenderFns: precompiled.staticRenderFns
});
But various attempts to rejigger that into something that can accept an input property have been unsuccessful (the following for example throws "Error in render function: ReferenceError: _c is not defined", I assume because the staticRenderFns aren't ready to go when render needs them?
Vue.component('test', {
props: ['input'],
render() { return Vue.compile(this.input).render()},
staticRenderFns() {return Vue.compile(this.input).staticRenderFns()}
});
(It's not because there are two separate compile()s -- doing the precompile inside beforeMount() and then returning its render and staticRenderFns throws the same error.)
This really feels like it's on the right track but I'm just stuck on a dumb syntax error or the like...
As mentioned in the my comment above, $compile was removed, but Vue.compile is available in certain builds. Using that below works as I believe you intend except in a couple cases.
Vue.component('child', {
// pretend I do something useful
template: '<span>--><slot></slot><--</span>'
})
Vue.component('parent', {
data() {
return {
input: 'lorem',
text: '<div><p>Lorem ipsum dolor sit amet.</p><p><i>Lorem ipsum!</i></p></div>'
}
},
template: `<div>
Search: <input type='text' v-model="input"><br>
<hr>
<div><component :is="output"></component></div>
</div>`,
computed: {
output() {
if (!this.input)
return Vue.compile(this.text)
/* This is the wrong approach; what do I replace it with? */
var out = this.text;
if (this.input) {
this.input = this.input.replace(/[^a-zA-Z\s]/g,'');
var regex = new RegExp(this.input, "gi");
out = out.replace(regex, '<child><b>' + this.input + '</b></child>');
out = Vue.compile(out)
}
return out;
}
}
});
new Vue({
el: '#app'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.0/vue.js"></script>
<div id="app">
<parent></parent>
</div>
You mentioned you are building with webpack and I believe the default for that build is Vue without the compiler, so you would need to modify it to use a different build.
I added a dynamic component to accept the results of the compiled output.
The sample text is not a valid template because it has more than one root. I added a wrapping div to make it a valid template.
One note: this will fail if the search term matches all or part of any of the HTML tags in the text. For example, if you enter "i", or "di" or "p" the results will not be what you expect and certain combinations will throw an error on compilation.
I'm posting this as a supplement to Bert Evans's answer, for the benefit of vue-cli webpack users who want to use .vue files instead of Vue.component(). (Which is to say, I'm mostly posting this so I'll be able to find this information when I inevitably forget it...)
Getting the right Vue build
In vue-cli 2 (and possibly 1?), to ensure Vue.compile will be available in the distribution build, confirm webpack.base.conf.js contains this line:
'vue$': 'vue/dist/vue.esm.js' // or vue/dist/vue.common.js for webpack1
instead of 'vue/dist/vue.runtime.esm.js'. (If you accepted the defaults when running vue init webpack you will already have the full standalone build. The "webpack-simple" template also sets the full standalone build.)
Vue-cli 3 works somewhat differently, and does not have Vue.compile available by default; here you'll need to add the runtimeCompiler rule to vue.config.js:
module.exports = {
/* (other config here) */
runtimeCompiler: true
};
The component
The "child" component can be a normal .vue file, nothing special about that.
A bare-bones version of the "parent" component would be:
<template>
<component :is="output"></component>
</template>
<script>
import Vue from 'vue';
import Child from './Child'; // normal .vue component import
export default {
name: 'Parent',
computed: {
output() {
var input = "<span>Arbitrary single-root HTML string that depends on <child></child>. This can come from anywhere; don't use unsanitized user input though...</span>";
var ret = Vue.compile(input);
ret.components = { Child }; // add any other necessary properties similarly
ret.methods = { /* ... */ } // like so
return ret;
}
}
};
</script>
(The only significant difference between this and the non-webpack version is importing the child, then declaring the component dependencies as ret.components: {Child} before returning it.)