Exposing functions on a Vue-defined Web Component (Custom Element) - vue.js

Per the Vue docs, it's possible to build components in Vue (v3) and package them as native Web Components for use with any framework or none at all.
As I've already found, the gap between design models for Vue components and Web Components can make this complex and sometimes a straight-up bad idea (at what point is it better and more maintainable to just go ahead building fully-native components?)... But let's assume for a moment that it's necessary here.
My question - What's the best way to expose a function-like interface on a Vue-built Web Component (to parent nodes)?
The Vue doc discusses passing in reactive data via props/slots, and publishing CustomEvents from the components, but I don't see mention of
taking function calls (or at a stretch, events) from outside. As far as I can tell this is a pretty strong assumption that data and event flow on the rest of the app/page works in a very "Vue-like way"?

For now, my workaround on this is to look up the host element in onMounted() (as per this question) and just set whatever extra properties are required at that point (hoping they shouldn't be required before the Vue component mounts, because I'm not aware of any external events raised when Vue finishes mounting the custom element).
This way the function can still be defined in the context of, and access variables/etc from, the setup function - but can be called by other elements on the page that only have a reference to the element, not the Vue component.
Can't say I like it much though:
<template>
<div ref="someElInTemplate">...</div>
</template>
<script lang="ts">
interface MyCoolHTMLElement extends HTMLElement {
myCoolFunction: () => void;
}
</script>
<script setup lang="ts">
const someElInTemplate = ref<HTMLElement>();
function myCoolFunction() { }
onMounted(() => {
const hostNode = (
somElInTemplate.value?.getRootNode() as ShadowRoot | undefined
)?.host as MyCoolHTMLElement;
hostNode.myCoolFunction = myCoolFunction;
});
</script>

Related

Why calling async function inside <script> tags fail in Vue if no lifecycle hooks are used?

Here is the scenario, I have a component, and inside the script tag I am calling an action
<script>
if (something is true) {
await store.doSomething()
}
</script>
The component fails to mount.
When I use the onMounted hook, it seems to work.
I am beginner in Vue, but my question is what is really happening when I don't use the hook? and is it always necessary to use hook when making asynchronous calls ?
Put it inside onMounted to get it to work, although ran into other test failures afterwards.
Looking at what you wrote as of right now, you should have the Options API like this
<script>
export default {
mounted() {
// your code
},
setup() {
// can also use setup here
}
}
</script>
With Composition API (notice the setup)
<script setup>
onMounted(() => {
// your code
})
</script>
In 2., if you don't use onMounted it will be run withing the setup lifecycle as shown here.
is it always necessary to use hook when making asynchronous calls ?
Not really, but at the same time it depends on when/how you want it to run. Start by running it into mounted initially yep, easier and safer to understand overall.
Especially since setup does not re-run when re-mounted, can be quite confusing.
It also depends exactly on what is something is true exactly, regarding the lifecycle + state.
Pinia and Vitest will get their own things to think about.
I recommend reading the documentation and getting an initial grasp before proceeding.

Vue 3: Composition API with external templates

So, I come from Angular world where by default we are separating view from business logic... and I don't like single file components, especially when they are getting bigger and convoluted.
Before in vue 2, I would have myComponent.vue with script tag and have it like so:
<template src="./myComponent.html"></template>
To call the view file... and it would work... all the methods were accessible
but now, if I use the setup way
<script setup lang="ts">
const suggets = () => {
}
</script>
<template src="./myComponent.html"></template>
I would get a typescript error that suggest was declared and never used...
Also for example:
<carbon-search /> component would not work
Failed to resolve component: carbon-search If this is a native custom
element, make sure to exclude it from component resolution via
compilerOptions.isCustomElement.
Any good and elegant way to split the component view and business logic (and css)?

Vue/Nuxt: How to make a component be truly dynamic?

In order to use a dynamically-defined single page component, we use the component tag, thusly:
<component v-bind:is="componentName" :prop="someProperty"/>
...
import DynamicComponent from '#/components/DynamicComponent.vue';
...
components: {
DynamicComponent
},
props: {
componentName: String,
someProperty: null,
}
The problem is, this isn't really very dynamic at all, since every component we could ever possibly want to use here needs to be not only imported statically, but also registered in components.
We tried doing this, in order at least to avoid the need to import everything:
created() {
import(`#/components/${this.componentName}.vue`);
},
but of course this fails, as it seems that DynamicComponent must be defined before reaching created().
How can we use a component that is truly dynamic, i.e. imported and registered at runtime, given only its name?
From the documentation: Emphasis mine
<!-- Component changes when currentTabComponent changes -->
<component v-bind:is="currentTabComponent"></component>
In the example above, currentTabComponent can contain either:
the name of a registered component,
or a component’s options object
If currentTabComponent is a data property of your component you can simply import the component definition and directly pass it to the component tag without having to define it on the current template.
Here is an example where the component content will change if you click on the Vue logo.
Like this:
<component :is="dynamic" />
...
setComponentName() {
this.dynamic = () => import(`#/components/${this.componentName}.vue`);
},
Solution for Nuxt only
As of now its possible to auto-import components in Nuxt (nuxt/components). If you do so, you have a bunch of components ready to be registered whenever you use them in your vue template e.g.:
<MyComponent some-property="some-value" />
If you want to have truly dynamic components combined with nuxt/components you can make use of the way Nuxt prepares the components automagically. I created a package which enables dynamic components for auto-imported components (you can check it out here: #blokwise/dynamic).
Long story short: with the package you are able to dynamically import your components like this:
<NuxtDynamic :name="componentName" some-property="some-value" />
Where componentName might be 'MyComponent'. The name can either be statically stored in a variable or even be dynamically created through some API call to your backend / CMS.
If you are interested in how the underlying magic works you can checkout this article: Crank up auto import for dynamic Nuxt.js components
According to the official Documentation: Starting from v2.13, Nuxt can auto import your components when used in your templates, to activate this feature, set components: true in your configuration
you are talking about async components. You simply need to use the following syntax to return the component definition with a promise.
Vue.component('componentName', function (resolve, reject) {
requestTemplate().then(function (response) {
// Pass the component definition to the resolve callback
resolve({
template: response
})
});
})

In Vue.js why do we have to export components after importing them?

In PHP when we include code from another file, we include it and that's it, the code is now available to us within the file in which we performed the include. But in Vue.js, after importing a component we must also export it.
Why? Why don't we simply import it?
in Vue.js, after importing a component we must also export it.
I think you might be referring to the following lines in User.vue and wondering why UserDetail and UserEdit are imported into the file and then exported in the script export's components property:
import UserDetail from './UserDetail.vue';
import UserEdit from './UserEdit.vue';
export default {
components: {
appUserDetail: UserDetail,
appUserEdit: UserEdit
}
}
vue-loader expects the script export of a .vue file to contain the component's definition, which effectively includes a recipe to assemble the component's template. If the template contained other Vue components, the definition of the other components would need to be provided, otherwise known as component registration. As #Sumurai8 indicated, the import of the .vue files itself does not register the corresponding single-file-components; rather those components must be explicitly registered in the importer's components property.
For example, if App.vue's template contained <user /> and User.vue were defined as:
<template>
<div class="user">
<app-user-edit></app-user-edit>
<app-user-detail></app-user-detail>
</div>
</template>
<script>
export default {
name: 'user'
}
</script>
...the User component would be rendered blank, and you would see the following console errors:
[Vue warn]: Unknown custom element: <app-user-edit> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
[Vue warn]: Unknown custom element: <app-user-detail> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
demo 1
When Vue attempts to render <user /> inside App.vue's template, Vue doesn't know how to resolve the inner <app-user-detail> and <app-user-edit> because their component registrations are missing. The errors can be resolved by local component registration in User.vue (i.e., the components property shown above).
Alternatively, the errors can be resolved with global component registration of UserDetail and UserEdit, which would obviate the local registration in User.vue. Note that global registration must be done before creating the Vue instance. Example:
// main.js
import Vue from 'vue';
import UserDetail from '#/components/UserDetail.vue';
import UserEdit from '#/components/UserEdit.vue';
Vue.component('app-user-detail', UserDetail);
Vue.component('app-user-edit', UserEdit);
new Vue(...);
demo 2
Components in vue can be tricky. If you haven't yet, I would highly recommend reading the documentation on the vue.js website on how component registration works, specifically, as tony19 mentions global and local registration. The code example you show in your screenshot is actually doing a couple of things. For one, it is making the components available locally, and only locally (as in that .vue file) for use. In addition, it is making it available to the template as the key you provide in the components object, in this case, app-user-detail and app-user-edit instead of user-detail and user-edit.
Importantly, it should be mentioned that an import is not actually required for this component registration to function. You could have multiple components defined in a single file. The components key gives a way to identify what that component is using. So that import isn't required, so vue does require the components key to understand what you are using as a component, and what is just other code.
Finally, as some of the other answers have alluded to, the components key is not actually an export. The default signature of a vue component requires an export but this is not exporting the components listed under the components key. What it is doing is letting vue build in a top down manner. Depending on what the rest of your application setup looks like, you may be using single file components, or not. Either way, vue will start with the top level vue instance and work its way down through components, with the exception of global registration, no top level component knows which components are being used below it.
This means for vue to render things properly, each component has to include a reference to the extra components it uses. This reference is exported as part of the higher level component (in your case User.vue), but is not the component itself (UserDetail.vue).
So it may appear that vue requires a second export after import, but it is actually doing something else to allow the root vue instance to render your component.
As an aside, the vue documentation on this subject really is quite good, if you haven't already please take a look at the sections I linked above. There is an additional section on module import/export systems that seems highly relevant to what you are asking, you can find that here: Module-systems.
import imports code into the current file, but it does not do anything on its own. Imagine the following non-vue code:
// File helpers.js
export function tickle(target) {
console.log(`You tickle ${target}`)
}
// File main.js
import { tickle } from 'helpers'
You have imported the code, but it does not do anything. To actually tickle something, you need to call the function.
tickle('polar bear');
In Vue this works the same. You define a component (or actually just an Object), but the component does not do anything on it's own. You export this component so you can import it in other places where the Vue library can do something with this object.
In a Vue component you export your current component, and import components you use in your template. You generally do the following:
<template>
<div class="my-component">
<custom-button color="red" value="Don't click me" #click="tickle" />
</div>
</template>
<script>
import CustomButton from './CustomButton';
export default {
name: 'my-component',
components: {
CustomButton
}
}
</script>
Your component mentions a component named "custom-button". This is not a normal html element. It does not know what to do with it normally. So what do we do? We import it, then put it in components. This maps the name CustomButton to the component you imported. It now knows how to render the component.
The "magic" happens when you mount the root component using Vue, usually in your main.js.
import Vue from "vue";
import App from "./App";
Vue.config.productionTip = false;
/* eslint-disable no-new */
new Vue({
el: "#app",
components: { App },
template: "<App/>"
});
What does this do? You tell Vue to render <App/> in a html element identified by #app, and you tell that it should find this element in ./App.vue.
But can't we just omit export if the Vue compiler was 'smarter'? Yes, and no. Yes, because a compiler can transform a lot of things into valid javascript, and no because it makes no sense and severely limits what you can do with your component, while also making the compiler more bug-prone, less understandable and overall less useful.
In order for App.vue to be able to use the User-component you need to export the default object of the User.vue-file.
In the export default { you don't actually export the newly imported components. You are just exporting a completely normal JavaScript Object. This object just happens to have a reference to another Object.
When you import an object (or function or array or ...) it does not actually load the content of that file in to your component like PHP. It simply makes sure that your compiler (probably webpack) knows how to structure the program. It basically creates a reference so webpack knows where to look for functionality.
TL;DR
The import and export here are conceptually different and unrelated things, and both have to be used.
Importing a Vue component is the same with any other importing in JavaScript:
// foo.mjs
export function hello() {
return "hello world!";
}
// bar.mjs
import { hello } from './foo.mjs';
console.log(hello());
Now run node bar.mjs, you will get a feeling how the importing works -- you want to use something that is defined/implemented somewhere else, then you have to import it, regardless of whether it is a Vue component or not.
With regard to export, you are not exporting the components you imported. The only thing you are exporting is the current component. However, this current component may use some other subcomponents in its <template>, so one has to register those subcomponents, by specifying them in the components field in the exported object.

Vue instance inside another Vue instance

I’m integrating Vue with a CMS called AEM thats works basically as component base system like Vue works too. But instead of having a webpack and imports of .vue files, every component on this CMS is a new Vue instance (new Vue({…})). So on my page I have a lot of Veu instances that communicate with each other using the same store (vuex).
This is actually working fine, but I have a scenario when I need a CMS component inside another. Since both this components are a unique vue instance and the “el” property from the parent includes the “el” from the child, the child component doesn’t work.
I know that this is not the expected use of this lib, but is there any way that I can tell or share the same “context” on both vue instances or even another approach for this scenario.
Thx,
Alexandre.
There should be only one instance of Vue.
I suggest you to create single empty Vue instance inside the body tag
All your existent Vue instances transform into components
Register all components in the root Vue instance
With this approach it will be fine to nest one component into another
You should use only one Vue instance as #shrpne mentioned.
If you keep instantiating Vue instances for every component, you'll run into issues while debugging or with component communication and overall this becomes very missy and you miss out on parent-child communication and inheritance provided by Vue.
I don't know about your Vue architecture, but I am currently working on a manual for working with Vue in AEM.
The basic premise is to use Vue's inline-template and vanilla-js, No typescript, nodeJS build, jsx or anything else at build time, just vanilla-js so that when your page is loaded and even before your js bundle is present, the DOM is already there, you just need to mount components by instantiating one Vue instance that will mount all components. This is also great for SEO (unless you plan to server-side render Vue components in java... which is possible theoretically, but good luck!)
Here is a sample AEM/Vue component:
<simple-counter inline-template>
<button v-bind:style="style" v-on:click="add">${properties.clicksText || 'clicks: '} {{ counter }}</button>
</simple-counter>
the JS:
notice how it does not have a template in the JS, because it's inlined above
Vue.component('simple-counter', {
data: function() {
return {
counter: 0,
style: {
color: 'red',
width: '200px'
}
}
},
methods: {
add: function() {
this.counter = this.counter + 1;
this.style.color = this.style.color == 'red' ? 'green' : 'red';
}
}
})
You can build more AEM components in this fashion, then at the end of your clientlib when all your Vue components have been registered, you can run:
new Vue({ el: '#app'})
This, off course, assumes that your page body or some other parent element has the id: app.
The second part of this, how do you enable re-mount of components after authoring dialog is submitted, you could just refresh the page.
I have a question about how we can re-mount components without refreshing the page here
The basic idea is to add an afteredit event to the component and run a new Vue instance only on the newly mutated component... still working on that
Solution:
Replace all new Vue(...) stuff into Vue.component(...) Vue.extend(...) etc for better interface management.
Only use ONE Vue instance witch is new Vue({...options})
Slice your vuex store into modules.
Google teacher knows everything.