(This is a further question after this one: In Vue, what is the relationship of template, render, VNode?)
Found createElement()'s source code (from here):
function createElement (
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
) { ... }
Code
main.js: (partly)
import App from './App.vue'
new Vue({
el: '#app',
render: h => h(App),
router
})
App.vue:
<template>
<div id="content">
<!-- <img src="./assets/logo.png" alt="">-->
<router-view></router-view>
</div>
</template>
<script>
export default {}
</script>
Questions
A. In App.vue, export default {} will return an empty object, is that correct?
B. In main.js:
B-1. import App from './App.vue', will bind App to the empty object, is that correct?
B-2. render: h => h(App), if the answer to previous two questions were yes, then here h (which is alias of vue's createElement() function) will get a single parameter, for context with value as empty object, is that correct?
B-3. But, that doesn't make much sense, because the template are actually rendered in the browser in my test, it's not empty.
C. So, in the code above, how many parameters are passed to createElement(), and what is the value of each of them?
D. Which part did I misunderstand about Vue or ES6 in above description?
Single-file components, i.e. .vue files, go through special processing by Vue Loader. See:
https://vue-loader.vuejs.org/#what-is-vue-loader
This will interfere in the export/import process so that everything works as expected.
So:
In App.vue, export default {} will return an empty object, is that correct?
It would for a .js file. But for a .vue file it won't, not once it's been through Vue Loader.
Try console logging the value of App in main.js to see what actual gets imported. You should see a render function (compiled from your template) as well a few other little supporting pieces.
Update
To answer your follow-up question from the comments.
createElement just creates a VDOM node. It does not create the component instance for that node. Vue further processes the tree of VDOM nodes after they are returned from render and creates any child component instances it needs. It'll then call render on those components to generate the VDOM nodes that they need and so on until everything is rendered.
I've created a small example to try to illustrate this. It consists of a parent with two component children. Notice that the logging for creating and rendering the child components does not happen straight away when calling createElement. Instead it happens after the render function has returned.
const child = {
render (createElement) {
console.log('rendering the child')
return createElement('div', null, ['hello'])
},
created () {
console.log('child is created')
}
}
new Vue({
el: '#app',
components: {
child
},
created () {
console.log('parent is created')
},
render (createElement) {
console.log('rendering the parent')
const children = [
createElement('child'),
createElement('child')
]
const root = createElement('div', null, children)
console.log('finished calling createElement')
return root
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app"></div>
In App.vue, export default {} will return an empty object, is that correct?
No, .vue files are Single File Components that are compiled by vue-loader.
B. In main.js:
B-1. import App from './App.vue', will bind App to the empty object, is that correct?
No, see above.
B-2. render: h => h(App), if the answer to previous two questions were yes, then here h (which is alias of vue's createElement() function) will get a single parameter, for context with value as empty object, is that correct?
No. App is a component ready to be attached to a Vue Instance. import App is simply importing the compiled file as an ES6 module.
C. So, in the code above, how many parameters are passed to createElement(), and what is the value of each of them?
If you console.log(App) you will see the compiled module (at minimum):
{
render: function render() {}
staticRenderFns: Array[0]
_compiled: true
beforeCreate: Array[2]
__file: "/src/App.vue"
beforeDestroy: Array[1]
_Ctor: Object
}
D. Which part did I misunderstand about Vue or ES6 in above description?
Both. Vue files are compiled to become Vue Instances. ES6 modules encapsulate all sorts of functionality, and expose this functionality to other JavaScript files, as libraries.
Related
I have a rather specific question.
I'm using vue in my rails application through rails webpacker, to use vue components, I have to put a javascript pack tag in my layout and that references a javascript file that in turn renders the vue component, you can imagine that in total this approach has led me to make a lot of workarounds, but the one thing I still have left is a vue custom directive click-outside that I have had to add to each of my vue component generators, for example, here in filter-products.js
import Vue from "vue";
import filterProducts from "../../views/filter-products";
var element = document.getElementById("filter-products");
const props = JSON.parse(element.getAttribute("props"));
Vue.directive('click-outside', {
bind: function(el, binding, vNode) {
//bind logic
},
unbind: function(el, binding) {
//unbind logic
}
});
if (element != null) {
new Vue({
render: (h) => h(filterProducts, { props }),
}).$mount(element);
}
the custom directive code is actually big, so what I have in mind but am not sure how to do is one of two things:
Have the bulk for that custom directive in an ES6 Module and import that here and just use it directly.
Create a prototype for Vue that includes this custom directive and import it instead of importing vue from "vue".
Is either of the approaches better? and how would I achieve them? thanks!
Create a folder named directives and for each directive create a file to make your code more organized and maintenable especially in team :
import Vue from 'vue';
const directiveName = {
inserted: function(el, binding) {},
update: function(el, binding) {},
};
export default directiveName;
Vue.directive('directiveName', directiveName);//optional
then import it in any component like :
import directiveName from 'path-to-directives-folder/directives/directiveName'
then use it as follows :
data(){
...
},
directives:{directiveName}
I've got this big legacy web app based on Codeigniter and jQuery.
The plan is to phase out jQuery and start using Vuejs instead. We are replacing certain parts of the website step by step.
I have just installed Nuxtjs and got the file structure the way i like it inside the legacy project.
Now to the question. How can i access a Vue component from one of my legacy javascript files?
App.vue
<template>
<div id="app">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</div>
</template>
<script lang="ts">
import Vue from "vue";
import HelloWorld from "./components/HelloWorld.vue";
export default Vue.extend({
name: "App",
components: {
HelloWorld
}
});
</script>
main.ts
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
Vue.config.productionTip = false;
new Vue({
store,
render: h => h(App)
}).$mount("#app");
I want to display my App.vue inside an ordinary php/html view.
I am doing something similar right now, the project was originally written using Thymeleaf and jQuery and now we are switching to Vue.
You can communicate between vue components and non-vue components in a few ways, none of them are "pretty".
Communication
Classic JavaScript events
Pretty straightforward
// legacy code
document.dispatchEvent(new CustomEvent('legacy-vue.something-happened', { detail: payload }))
// vue component
created () {
document.addEventListener('legacy-vue.something-happened', this.onSomethingHappened)
},
destroyed () { // don't forget to remove the listener!
document.removeEventListener('legacy-vue.something-happened', this.onSomethingHappened)
}
Exposing EventHub
Similar to the previous one, but you are using vue events instead. This is what i would recommend, because it's the Vue way of handling events and your goal is to vuetify your app.
// initialization
const hub = new Vue()
Vue.prototype.$eventHub = hub
window.$vueEventHub = hub
// legacy code
$vueEventHub.$emit('something-happened', payload)
// vue component
created () {
this.$eventHub.$on('something-happened', this.onSomethingHappened)
},
destroyed () {
this.$eventHub.$off('something-happened', this.onSomethingHappened)
}
Exposing whole components
The most flexible way, but it's hard to see what's going on where. Event based approaches the lesser evil in my opinion (it's easy to track events).
// vue component
created () {
window.vueTableComponent = this
}
// legacy component
vueTableComponent.fetchNextPage()
vueTableComponent.registerOnPageFetchedCallback(callback);
Summary
No matter which approach you pick, i recommend doing something like this:
Let's say that you have TableComponent. TableComponent has few props like apiUrl, emits row-clicked event, etc. It would be best to design the component without thinking about your legacy code at all, and then create it's legacy wrapper because at one point it will be used both with vue-only screens and with mixed-screens (with both legacy components and vue components). An example:
LegacyTableComponentWrapper.vue
<template>
<table-component
:api-path="apiPath"
#row-clicked="onRowClicked"
ref="table-component"
/>
</template>
export default {
data: () => ({
apiPath: null
}),
methods: {
onRowClicked (row) {
this.$eventHub.$emit('table-row-clicked', row) // notify legacy code
},
onApiPathDefined (payload) {
this.apiPath = payload
}
},
mounted () {
// legacy code might require the TableComponent to act differently
// and if you don't want the TableComponent to know whenever it's legacy or not
// you can always override one or more of it's methods.
this.$refs['table-component'] = this.overriddenImplementationOfXYZ
},
created () {
this.$eventHub.$on('define-api-path', this.onApiPathDefined)
},
destroyed () {
this.$eventHub.$off('define-api-path', this.onApiPathDefined)
}
}
It's sure more work at first, but will save you a headache later, when you will be working on your first view which is fully in vue and all that legacy communication stuff is in the way.
I'm using vuejs with typescript, but the question would apply with javascript too.
With vue-test-utils, components are tested using mount or shallowMount. This way, I've been able to unit test the App main component. I'm now wondering how I can unit test the main.ts file, which does already mount the main component:
new Vue({
router,
store,
i18n,
render: (h) => h(App),
}).$mount('#app');
The unit test would check if App component is really mounted into #app.
Unsurprisingly, if I just import main.ts in my test, I get this error:
Cannot find element: #app
Is it possible to do something to create a fake DOM containing an #app element, in which the App component would be mounted?
You might use Jest to mock a DOM. This way you could setup multiple tests:
initializing: *Verify that without any parameters it can still instantiate the Vue instance (undefined as router, store, etc). Verify that the properties like router etc are added to the prototype
Mounting: Create a mock App which renders to your created DOM and verify that the app renders as expected to the correct element
Especially mounting feels more like functional testing instead of unit testing (as it performs more than a 'unit' of work)
EDIT: provide example
// __tests__/main.test.js
'use strict';
test('Can mount app', () => {
document.body.innerHTML =
'<div id="app">' +
'</div>';
// Executes main file
require('../src/main');
const pElement = document.getElementById('example');
expect(pElement).toBeTruthy();
expect(pElement.textContent).toEqual('Example');
});
With a main file of:
// main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false;
new Vue({
render: h => h(App),
}).$mount('#app');
And a App.vue file of:
<template>
<div id="app">
<p id="example">Example</p>
</div>
</template>
Rewriting to Typescript is trivial. A few points to notice:
The App itself is not mocked (Vue.app as injected in main.js). This can be done by creating a separate mock file or using jest.fn() although I have not tested this myself.
The tests are not functional testing it but with small changes can be made to verify small units of work
I currently have three steps in a form that I want to show sequentially, so I created three components - one for each step of the process.
My app.js file:
import LocationList from './components/LocationList.vue';
import ChooseTime from './components/ChooseTime.vue';
import ChooseMethod from './components/ChooseMethod.vue';
Vue.component('location-list', LocationList);
Vue.component('choose-time', ChooseTime);
Vue.component('choose-method', ChooseMethod);
let store = {
isVisible: {
steps: {
one: true,
two: false,
three: false,
}
}
};
new Vue({
el: '#app-order',
data: store,
router
});
Now, when my one and only route is called,
import VueRouter from 'vue-router';
let routes = [
{
path: '/order',
component: require('./views/Order.vue')
}
];
export default new VueRouter({
routes
});
all these components are being loaded properly. The issue is that when I try to v-show them one at a time:
Order.vue:
<template>
// ...
<location-list v-show="isVisible.steps.one"></location-list>
<choose-time v-show="isVisible.steps.two"></choose-time>
<choose-method v-show="isVisible.steps.three"></choose-method>
// ...
</template>
<script>
</script>
<style>
</style>
The error message I receive is:
[Vue warn]: Property or method "isVisible" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
But when I check within Vue's browser extension, isVisible is defined within the root element?
As you can see it is in the root-element, but not inside the Order view though.
Thanks for any help!
In Vue, child components do not have direct access to data defined in their parents. You have to pass the data down.
I think you would probably save yourself a little trouble if you just defined isVisible in Order.vue. However, if you want to leave it where it is, you need to pass it into the component.
One easy way to do that is to define isVisble as a property of Order.vue and then pass it through your router-view.
<router-view :is-visible="isVisible"></router-view>
There are other ways of passing props to routes that are defined in the router documentation.
The reason I say you would save your self some trouble defining isVisible in Order.vue is because whenever you want to change the values of your steps, you will need to do it at the root as you currently have it defined.
I've just started working with Vue.JS and there's one small issue that's bugging me. My file structure similar to the following:
+ js
|--+ components
| |-- parent.vue
| |-- child.vue
|-- main.js
Then in my main.js I have the following:
window.Vue = require('vue');
require('vue-resource');
Vue.component('parent', require('./Components/parent'));
Vue.component('child', require('./Components/child'));
var app = new Vue({ el: "#app" });
(I'm not actually certain what vue-resource is, but this was set up for me by a fresh install of Laravel 5.3)
At a glance I immediately noticed that my main.js file was going to get unmanageable if I added too many components. I don't have this issue when working with ReactJS because main.js only needs to include the "parent" component, and the parent component includes the child component. I figured Vue.JS would have a similar trick to help me organize my components - but reading through the docs I didn't find one (maybe I missed it?)
Is there a way to either have a Vue component list its dependencies (for Browserify / Webpack to bundle) or recursively run a javascript statement on every file in a directory (so Browserify / Webpack just packs up the whole thing)?
I'm not concerned with async components at the moment - so if the solution breaks that functionality it will be okay. One day I would like to play around with using Webpack to create async components and only loading them as I need them, but today I'm more interested in just getting this up and running so I can play way Vuex.
The Vue.component syntax is for global components only, if you have a component that is being used inside another component use this:
import Parent from './components/Parent.vue';
import Child from './components/Child.vue';
new Vue({
el: "#app",
components: { Parent, Child }
});
Than inside this components you can use the other components.
The only advantage of using Vue.component(Parent) is that you can use this <parent></parent> component globaly in all your other components without declaring them implicitly.
Good Luck :)
You don't need to import everything at the top level.
In your main.js you can import the Parent component
import Parent from './components/Parent.vue'
new Vue({
el: "#app",
components: {
Parent
}
})
With your Parent.vue
<template>
<div>
<p>I am the parent</p>
<child></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
mounted() {
console.log('mounted parent')
}
}
</script>
<style scoped>
// ...
</style>
Then in your Child.vue
<template>
<p>I am the child</p>
</template>
<script>
export default {
mounted() {
console.log('mounted child')
}
}
</script>
<style scoped>
// ...
</style>
And you should end up with
<div>
<p>I am the parent</p>
<p>I am the child</p>
</div>
I found a way, not sure if it's the best in terms of performance and webpack chunk size. I created an index.js file in the components root:
export const HelloWorld = require('./HelloWorld.vue').default
So, inside the components I would use:
const { HelloWorld } = require('#/components')
Due to babel issues I need to make a mix of require and export, also the use of default attribute after require — as I read in some babel use discussions.