Skip to content

Commit d06a7a1

Browse files
committed
angular component inside vue
1 parent ead2f89 commit d06a7a1

File tree

9 files changed

+209
-13
lines changed

9 files changed

+209
-13
lines changed

README.md

+157-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
**==> Work in progress, stay tuned! <==**
2-
----
3-
4-
5-
# Migrating an Angular 1.x app to Vue
1+
# Migrating an Angular 1.x app to Vue (it's a long way to the top if you wanna ronck'n'roll)
62
> Github repository: https://github.com/arcadeJHS/AngularVueIntegration.
73
84
Sometimes you have to say "stop!" and decide it's time to migrate to a warmer and sunnier place.
@@ -163,7 +159,7 @@ code
163159
|_index.html
164160
```
165161

166-
> **Please note:** here I will not initialize the Vue app through vue-cli. I am reusing a Webpack custom configuration which suites my needs. Nevertheless, everything should work the same way if you are using vue-cli.
162+
> **Please note**: here I will not initialize the Vue app through vue-cli. I am reusing a Webpack custom configuration which suites my needs. Nevertheless, everything should work the same way if you are using vue-cli.
167163
168164
See tag **`tag-02-app-directory-structure`** (with emtpy folders and files).
169165

@@ -1339,6 +1335,157 @@ Sure, you can probably do the same with the `store.searchResults` property. Our
13391335
Refer to `tag-08-more-store`.
13401336

13411337

1338+
Conclusions
1339+
----
1340+
As you have seen, once you grasp a way (I am not claiming here mine is the best or the only one) to let Angular 1.x and Vue cohabit things get easier, and you can resort to a methodology for migrating your codebase progressively.
1341+
1342+
> "I belong to the warrior in whom the old ways have joined the new."
1343+
>
1344+
> <em>(The Last Samurai)</em>
1345+
1346+
Again, what has been exposed in this article reflects only my opinions, and do not, in any way, constitute the best or only way to achieve the ultimate goal of renewing an old application by completely removing Angular code.
1347+
1348+
Oh, one more thing...
1349+
1350+
1351+
Angular component nested inside Vue component
1352+
----
1353+
Ok, you got me! What about requirement #2? What happened to `inner-detail` once you migrated to `vue-app-container`?
1354+
I have to be honest here, and admit we must be brave and really creative to solve the last puzzle.
1355+
1356+
> "That's my friend, Irishman. And the answer your question is yes - if you fight for me, you get to kill the Angular."
1357+
>
1358+
> <em>(William Wallace, Braveheart)</em>
1359+
1360+
As confirmed by one of the main repository contributors, ngVue was not designed to allow AngularJS components to be rendered inside Vue components. Someone has tried to solve the problem using `slots`, but, due to rendering differences between the frameworks, the implementation is buggy (and not recommended, because maybe in the future will be deprecated, as stated by [issue #66][27]).
1361+
After a brief discussion (see [issue #79 on GitHub][26]), thanx to tips coming from all the participants involved (and a previous experience with Angular `injector`), I overcame the problem the way I will tell below.
1362+
It seems to work, but it also is somehow experimental (I simply lack a deep knowledge on the subject, and I am not completely aware of possible unwanted side effects). Hence I am not sure I would really recommend it.
1363+
Anyway, to me nesting Angular components inside Vue was an essential requirement; so I report it here to complete the picture, and give a possible solution.
1364+
1365+
**TL;DR**: I cooked up a Vue component which wraps and compiles an Angular component, and quietly listen for changes in the scope bound.
1366+
1367+
**ngVueBridgeCode/components/AngularComponent.vue**
1368+
```html
1369+
<template>
1370+
<div></div>
1371+
</template>
1372+
1373+
<script>
1374+
import angular from 'angular';
1375+
import SafeApply from '@/ngVueBridgeCode/utilities/safeApply';
1376+
1377+
let ctrlUnwatch;
1378+
1379+
export default {
1380+
name: "AngularComponent",
1381+
props: ['component'],
1382+
mounted () {
1383+
const el = angular.element(this.$el);
1384+
let scope;
1385+
1386+
el.injector().invoke(['$compile', '$rootScope', ($compile, $rootScope) => { // #1
1387+
scope = angular.extend($rootScope.$new(), { $ctrl: this.component.$ctrl }); // #2
1388+
el.replaceWith($compile(this.component.template)(scope)); // #3
1389+
}]);
1390+
1391+
ctrlUnwatch = this.$watch('component.$ctrl', (ctrl) => { // #4
1392+
scope.$ctrl = angular.merge(scope.$ctrl, ctrl); // #5
1393+
SafeApply.call(scope); // #6
1394+
}, { deep: true });
1395+
},
1396+
destroyed () {
1397+
ctrlUnwatch(); // #7
1398+
}
1399+
};
1400+
</script>
1401+
```
1402+
1403+
A lot of stuff in a few lines.
1404+
1405+
**#1**: `injector` is an Angular object that can be used for retrieving services as well as for dependency injection (see the [official documentation][28]). Here we are accessing to it to inject and compile a component on the fly, after the Angular application has already been bootstrapped.
1406+
1407+
**#2**: here we are setting the scope we will bind to the component template, extending a fresh `$scope` with the `$ctrl` object passed by the `component` prop, which basically is an object like this:
1408+
```javascript
1409+
{
1410+
template: '<some-angular-component some-binding="$ctrl.someBinding"></some-angular-component>',
1411+
$ctrl: { someBinding: 'foo' }
1412+
}
1413+
```
1414+
1415+
**#3**: we replace the `<div/>` tag in the Vue template with the compiled Angular component.
1416+
1417+
**#4**: as already seen previously, we are entangled to Angular $digest loop. To inform Angular something has changed in the object associated to its current scope, update bindings, and re-render, we are introducing a `watcher` on the `component.$ctrl` prop. Note the `{ deep: true }` option, to trigger the watcher in case you have got a complex nested object.
1418+
1419+
**#5**: any time the prop changes, we update the scope by merging the new object `ctrl` with the existing `scope.$ctrl` - `angular.merge` performs a **deep copy**, which is what we need here to be sure to propagate all the updates.
1420+
1421+
**#6**: and any time the prop changes, we call our old friend `SafeApply`, bound to an updated scope, to start a `$digest`.
1422+
1423+
**#7**: `this.$watch` return a function we can use to clear the watcher when the component got destroyed.
1424+
1425+
Then you simply use the component wherever you want to inject an Angular component:
1426+
1427+
**vueCode/components/Detail/index.vue**
1428+
```html
1429+
<template>
1430+
<div class="app-Detail" v-if="currentDetail">
1431+
<!-- ... -->
1432+
<angular-component :component="innerDetail"></angular-component>
1433+
</div>
1434+
</template>
1435+
<script>
1436+
import AngularComponent from '@/ngVueBridgeCode/components/AngularComponent.vue';
1437+
export default {
1438+
/* ... */
1439+
components: {
1440+
AngularComponent
1441+
},
1442+
data () {
1443+
return {
1444+
currentDetail: null
1445+
}
1446+
},
1447+
computed: {
1448+
innerDetail () {
1449+
return {
1450+
template: '<inner-detail inner-data="$ctrl.innerData"></inner-detail>',
1451+
$ctrl: { innerData: this.currentDetail.more }
1452+
};
1453+
}
1454+
},
1455+
/* ... */
1456+
};
1457+
</script>
1458+
```
1459+
1460+
`innerDetail` is the `component` prop we have previously introduced. It is better to define it as a computed property to get it correctly initialized.
1461+
1462+
**Please note**: I have found that in order to have the Angular component completely working, you nee to define its HTML template in a separate file:
1463+
1464+
```javascript
1465+
templateUrl: 'angularApp/components/innerDetail.html'
1466+
```
1467+
1468+
Maybe it depends on how and when things are getting parsed and compiled.
1469+
For instance, if you write:
1470+
1471+
```javascript
1472+
template: '<div class="app-InnerDetail">{{$ctrl.title}}: <p>{{$ctrl.innerData}}</p></div>'
1473+
```
1474+
1475+
things will not completely work, and you end up having on screen an unresolved template:
1476+
1477+
```html
1478+
{{$ctrl.title}}:
1479+
{{$ctrl.innerData}}
1480+
```
1481+
1482+
If you now rebuild and launch the application you can check the `innerDetail` component is working as expected:
1483+
1484+
![angular-component-inside-vue][29]
1485+
1486+
Refer to **`tag-09-angular-component-inside-vue`**.
1487+
1488+
13421489

13431490
[1]: screenshots/01-simple_app.png
13441491
[2]: screenshots/02-app_components.png
@@ -1365,3 +1512,7 @@ Refer to `tag-08-more-store`.
13651512
[23]: screenshots/08-safe_apply.png
13661513
[24]: screenshots/09-event_bus.png
13671514
[25]: screenshots/10-event_bus_triggers.png
1515+
[26]: https://github.com/ngVue/ngVue/issues/79
1516+
[27]: https://github.com/ngVue/ngVue/issues/66
1517+
[28]: https://docs.angularjs.org/api/ng/function/angular.injector
1518+
[29]: screenshots/11-angular_component_inside_vue.png
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div class="app-InnerDetail">{{$ctrl.title}}: <p>{{$ctrl.innerData}}</p></div>

code/angularApp/components/innerDetail.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
'use strict';
33

44
angular.module('AngularApp').component('innerDetail', {
5-
template:
6-
'<div class="app-InnerDetail">{{$ctrl.title}}: <p>{{$ctrl.innerData}}</p></div>',
5+
templateUrl: 'angularApp/components/innerDetail.html',
76
bindings: {
87
innerData: '<'
98
},

0 commit comments

Comments
 (0)