I have an AngularJS application where I'm using few components of Vue (version 2).
The Navigation bar of the app is in AngularJS.
When I switch between different navigation options (Home/Support), I'm loading the related Vue components.
VueJS project is built using npm run buid-lib and linked in angularJS app (npm link). Library name is myVueApp.
Using ngVue library to integrate Vue with AngularJS.
Code to load Vue Components in AngularJS app (as plugin):
(function() {
'use strict';
if (typeof myVueApp !== 'undefined') {
var VueContainer = myVueApp.components.VueContainer,
plugins = myVueApp.plugins;
}
angular
.module('my.vue.config', ['ngVue', 'ngVue.plugins'])
.config(function($ngVueProvider) {
$ngVueProvider.setRootVueInstanceProps(plugins);
})
// Add Vue components here
.value('VueContainer', VueContainer);
})();
In HTML page, I'm loading the component like below example:
Home.Html (In AngularJS)
<div>
<vue-component name="VueContainer"></vue-component>
</div>
Routing of the Vue Components in Angular Controller:
var ngModule = angular.module('home.content', ['ui.router']);
ngModule.config(function ($stateProvider) {
$stateProvider
.state('home.content', {
url: '?home&t',
controller: 'HomeCtrl as HomeCtrl ',
templateUrl: '/views/home.html',
resolve: {
},
onEnter: function(){
}
});
When I switch between Vue Components, the Created/Mounted event of the each Vue component (callee and called) is getting called.
Example Vue Components:
HomeComponent.Vue
async created() { //This is called every time it is switched to other component
await this.getHomeData();
this.loading = false;
}
SupportComponent.Vue
created() { //This is called every time it is switched to other component
this.$store.dispatch('getAllSupportProp');
this.loadDetails();
}
In angular app, when I swtich from HomeComponent.Vue to SupportComponent.Vue or vice-versa, created/mounted events of both the components are getting called while it should only call the target component's created event.
When I run the Vue app alone (not with AngularJS), the switching between components doesn't have this problem (Callee component doesn't get called).
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 am converting an AngularJS project to Vue 2.
My strategy is to have AngularJS handle routing, and slowly swap out individual components within each angular view.
In my main.ts file, I'm importing and defining global components like this:
import LoginForm from './components/LoginForm.vue';
Vue.component('LoginForm', LoginForm);
When the relevant Angular controller loads, I'd like to run some javascript to manually mount the Vue component.
Is this a good approach?
How can I manually mount a previously registered component?
Vue.somehowGetComponent('LoginForm').mountTo('#login-form');
I wouldn't register it as a global component for this purpose. Simply import it, then create an instance of it and mount it. eg.
// import the component
import LoginForm from './components/LoginForm.vue';
// If you haven't already, must call `Vue.extend` on LoginForm before its instantiation
const myLoginForm = new LoginForm();
// mount to your legacy app
myLoginForm.$mountTo(document.getElementById('login-form'));
The key here was calling Vue.extend on my imported component:
In Vue.js:
// main.ts
import LoginForm from './components/LoginForm.vue';
window.LoginForm = Vue.extend(LoginForm)
Then in the AngularJS controller:
angular.module('app').controller(function(){
var vue = new window.LoginForm();
vue.$mount('#login-form');
});
At this project I'm working on there is a legacy server-rendered web page and some components had problems I've been assigned to fix, and I convinced the team to rewrite those parts in Vue to kickstart our migration.
I wrote the whole mini-app using the Webpack template provided by Vue CLI and it works like a charm... in that specific environment.
If I npm run build the built index.html also works fine in a static server.
However, I can't seem to include the app in an existing page composed of many other elements. Shouldn't it be as simple as adding the <div id='myApp'></div> element to the HTML and loading the generated JS files?
If it helps, the legacy app is a Rails app using .erb templates and the JS files are being loaded through the main pipeline in application.js.
Does anyone know why nothing happens when I try this?
Edit: more information - this is how main.js looks before build:
/* eslint-disable */
import Vue from 'vue'
// UI components
import VueSelect from 'vue-select'
import DynamicForm from './components/DynamicForm/'
Vue.component('vue-select', VueSelect)
Vue.config.productionTip = false
const DynamicForms = new Vue({
el: '.dynamic-form',
render: h => h(DynamicForm)
})
Edit: I managed to get Vue to work by integrating Webpack to Rails with Webpacker. However I still have some problems regarding context:
This is my main.js in one of the Vue components. It was working fine until I tried the PropData stunt so I could reuse the component with different data in a few places.
/* eslint-disable */
import Vue from 'vue'
// UI components
import VueSelect from 'vue-select'
// import 'nouislider'
import DynamicForm from './components/DynamicForm/'
import fields from './fields'
import fieldRules from './field-rules'
Vue.component('vue-select', VueSelect)
Vue.config.productionTip = false
document.addEventListener('DOMContentLoaded', () => {
const el = document.createElement('div')
document.querySelector('.dynamic-form').appendChild(el)
const vm = new DynamicForm({
propsData: {
fields,
fieldRules
},
el,
render: h => h(DynamicForm)
})
})
This is DynamicForm/index.vue
<template>
<div id='app'>
<ParamList :fields='paramFields' :fieldRules='paramRules'></ParamList>
<Output></Output>
</div>
</template>
<script>
import Vue from 'vue'
import ParamList from './ParamList'
import Output from './Output'
export default Vue.extend({
props: [ 'fields', 'fieldRules' ],
name: 'DynamicForm',
components: {
ParamList,
Output
},
data () {
return {
paramFields: this.fields,
paramRules: this.fieldRules
}
}
})
</script>
<style>
</style>
The field and fieldData props are merely JSON/JSONP with some data I'm going to use inside those components. The idea is that I could write another main.js changing just the field and fieldData when initing the Vue instance.
What am I doing wrong?
I've managed to fix everything in a three-step change to my components.
Integrate Webpack into Rails using Webpacker. There's even a Vue template!
Change the root component (the one mounted at a real DOM element) to a Vue subclass using Vue.extend (so the module line # the .vue file read export default Vue.extend({ instead of simply export default {
Remove the render function from the new DynamicForm (the name I assigned Vue.extend to) so it renders its own template.
I hope it helps as it was quite a pain to me!
With angular 5.0 the upgrade module now has the option of using downgradeModule which runs angularjs outside of the angular zone. While experimenting with this I have run into a problem with using downgradeInjectable.
I am receiving the error:
Uncaught Error: Trying to get the Angular injector before bootstrapping an Angular module.
Bootstrapping angular in angular js works fine
import 'zone.js/dist/zone.js';
import * as angular from 'angular';
/**
* Angular bootstrapping
*/
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { decorateModuleRef } from 'src/environment';
import { AppModule } from 'src/app/app.module';
import { downgradeModule } from '#angular/upgrade/static';
export const bootstrapFn = ( extraProviders ) => {
const platformRef = platformBrowserDynamic( extraProviders );
return platformRef
.bootstrapModule( AppModule )
.then( decorateModuleRef );
};
angular.module( 'app.bootstrap', [
downgradeModule( bootstrapFn ),
] );
However...
Since the bootstrapping takes place after angularjs has been initialized I can no longer get the downgrade injectable working.
Service to be downgraded
import { Injectable, Inject, OnInit } from '#angular/core';
#Injectable()
export class MobileService implements OnInit{
constructor(
#Inject( 'angularjsDependency1' ) public angularjsDependency1 : any,
#Inject( 'angularjsDependency2' ) public angularjsDependency2 : any,
) {}
}
Downgrade injectable attempt
import * as angular from 'angular';
import { downgradeInjectable } from '#angular/upgrade/static';
import { MyService } from 'src/services/myService/myService';
export const myServiceDowngraded = angular.module( 'services.mobileService', [
angularjsDependency1,
angularjsDependency2,
] )
.factory(
'mobileService',
downgradeInjectable( MyService ),
).name;
When "downgradeInjectable( MyService ) runs the angular injector is not available yet since angular hasn't been bootstrapped. Hence the error:
Uncaught Error: Trying to get the Angular injector before bootstrapping an Angular module.
Does anyone have an idea how I might fix this?
Answers in this thread helped me find a solution, but none contains the holy grail:
Creating a service-boostrap component aside the app's code does not work, because Angular is loaded asynchronously, unlike AngularJS. This gives the same error Trying to get the Angular injector before bootstrapping an Angular module.
Creating a service-bootstrap component wrapping the AngularJS code kind of worked, but then I experienced issues with change detection inside Angular composants, as described in this issue on github.
In the github issue, someone suggested to edit #angular/upgrade source code to change a false to true to force components to be created in the Zone. But in this case it seems to cause performance issues (it seemed to launch ngZone's code multiple times on user events)
In order for the app to work correctly, I needed :
Not to have ng components containing AngularJS components containing Angular components. We need to only have AngularJS containing Angular components.
Make sure that AngularJS components using Angular services are created after a first angular component, named service-bootstrap
To acheive this, I created a slightly modified service-bootstrap component:
import { Component, Output, EventEmitter, AfterViewInit } from "#angular/core";
#Component({
selector: 'service-bootstrap',
template: ``
})
export class ServiceBootstrapComponent implements AfterViewInit{
#Output()
public initialized: EventEmitter<void> = new EventEmitter();
public ngAfterViewInit(){
this.initialized.emit();
}
}
Declared this component as entryComponent in the Angular module and called downgradeComponent to register it in AngularJS:
import { downgradeModule, downgradeInjectable, downgradeComponent } from '#angular/upgrade/static';
const bootstrapFn = (extraProviders: StaticProvider[]) => {
const platformRef = platformBrowserDynamic(extraProviders);
return platformRef.bootstrapModule(AppModule);
};
const downgradedModule = downgradeModule(bootstrapFn);
const app = angular.module('ngApp', [
downgradedModule,
'app'
]);
app.directive('serviceBootstrap', downgradeComponent({ component: ServiceBootstrapComponent }));
Then (and the magic happens here), I created a new AngularJS component:
angular.module("app")
.directive('ng1App', ng1AppDirective);
function ng1AppDirective(){
return {
template: `
<service-bootstrap (initialized)="onInit()"></service-bootstrap>
<section ng-if="initialized">
<!-- Your previous app's code here -->
</section>
`,
controller: ng1AppController,
scope: {},
};
}
ng1AppController.$inject = ['$scope'];
function ng1AppController($scope){
$scope.onInit = onInit;
$scope.initialized = false;
function onInit(){
$scope.initialized = true;
}
}
Then, my index.html only referenced this component
<body>
<ng1-app></ng1-app>
</body>
With this approach, I'm not nesting AngularJS components inside Angular components (which breaks change detection in Angular components), and still I ensure that a first Angular component is loaded before accessing the Angular providers.
Note: The answer below follows the convention of calling angular 1.x as angularjs and all angular 2+ versions as simply angular.
Expanding on JGoodgive's answer above, basically, if you're using downgradeModule, then angular module is bootstrapped lazily by angularjs when it needs to render the first angular component. Until then, since the angular module isn't initialised, if you are accessing any angular services inside angularjs using downgradeInjectable, those services aren't available too.
The workaround is to force bootstrapping of the angular module as early as possible. For this, a simple component is needed:
import {Component} from '#angular/core';
#Component({
selector: 'service-bootstrap',
template: ''
})
export class ServiceBootstrapComponent {}
This component doesn't do anything. Now, we declare this component in the top level angular module.
#NgModule({
// ...providers, imports etc.
declarations: [
// ... existing declarations
ServiceBootstrapComponent
],
entryComponents: [
// ... existing entry components
ServiceBootstrapComponent
]
})
export class MyAngularModule {}
Next, we also need to add a downgraded version of this component to angularjs module. (I added this to the top level angularjs module I had)
angular.module('MyAngularJSModule', [
// ...existing imports
])
.directive(
'serviceBootstrap',
downgradeComponent({ component: ServiceBootstrapComponent }) as angular.IDirectiveFactory
)
Finally, we throw in this component in our index.html.
<body>
<service-bootstrap></service-bootstrap>
<!-- existing body contents -->
</body>
When angularjs finds that component in the markup, it needs to initialise angular module to be able to render that component. The intended side effect of this is that the providers etc. also get initialised and are available to be used with downgradeInjectable, which can be used normally.
This was pointed out to me in an angular github thread.
https://github.com/angular/angular/issues/16491#issuecomment-343021511
George Kalpakas's response:
Just to be clear:
You can use downgradeInjectable() with downgradeModule(), but there are certain limitations. In particular, you cannot try to inject a downgraded injectable until Angular has been bootstrapped. And Angular is bootstrapped (asynchronously) the first time a downgraded component is being rendered. So, you can only safely use a downgraded service inside a downgraded component (i.e. inside upgraded AngularJS components).
I know this is limiting enough that you might decide to not use downgradeInjectable() at all - just wanted to make it more clear what you can and can't do.
Note that the equivalent limitation is true when using an upgraded injectable with UpgradeModule: You cannot use it until AngularJS has been bootstrapped. This limitation usually goes unnoticed though, because AngularJS is usually bootstrapped in the Angular module's ngDoBootstrap() method and AngularJS (unlike Angular) bootstraps synchronously.
I had the same issue, and the reasons are explained in the above answer.
I fixed this by dynamically injecting the downgraded angular service using $injector.
Steps
Register your downgraded service to angularjs module
angular.module('moduleName', dependencies)
angular.factory('service', downgradeInjectable(Service));
Inject $injector to your controller and use this to get the downgraded service
const service = this.$injector.get('service');
service.method();
I had the same issue and it sucked up several hours before finding this.
My workaround was to create a ServiceBootstrapComponent that does nothing but injects all the services that we need to downgrade.
I then downgrade that component, mark it as en entry in #NgModule and add it to index.html.
Works for me.
I was getting the same error in our hybrid app. We are using the following versions:
AngularJS 1.7.x
Angular 7.3.x
As mentioned in this answer, I also used a dummy component called <ng2-bootstrap> to force boostrapping of Angular. And then, I created an AngularJS service which checks if Angular has been bootstrapped:
// tslint:disable: max-line-length
/**
* This service can be used in cases where Angular fails with following error message:
*
* `Error: Trying to get the Angular injector before bootstrapping the corresponding Angular module.`
*
* Above error occurs because of how `downgradeModule` works.
*/
/*#ngInject*/
export class Ng2BootstrapDetectionService {
private bootstrapDone = false;
constructor(private $q: ng.IQService) {}
public whenBootstrapDone(): ng.IPromise<void> {
if (this.bootstrapDone) {
return this.$q.resolve();
}
const deferred = this.$q.defer<void>();
angular.element(document).ready(() => {
const intervalId = setInterval(() => {
const el = document.querySelector('ng2-bootstrap');
if (el && el.outerHTML.includes('ng-version=')) {
this.bootstrapDone = true;
clearInterval(intervalId);
deferred.resolve();
}
}, 500);
});
return deferred.promise;
}
}
Ng2BootstrapDetectionService can be used like below:
import {NotificationService} from 'ng2-app/notification.service';
// This can be used in cases where you get following error:
// `Error: Trying to get the Angular injector before bootstrapping the corresponding Angular module.`
// You will need access to following
// $injector: AngularJS Injector
// Ng2BootstrapDetectionService: our custom service to check bootsrap completion
this.Ng2BootstrapDetectionService
.whenBootstrapDone()
.then(() => {
const notificationService = this.$injector
.get<NotificationService>('ng2NotificationService');
notificationService.notify('my message!');
});
You can find more details about this solution at the end of this blog post.