Message Boards

Liferay DXP: SennaJS and Angular 1 incompatible?

Zak Thompson, modified 7 Years ago.

Liferay DXP: SennaJS and Angular 1 incompatible?

Junior Member Posts: 70 Join Date: 6/13/16 Recent Posts
In the course of attempting to get angular 1 to work in Liferay DXP for creating custom directives, I think I discovered a fatal flaw that renders the two inoperable together. The bug is that when an anchor tag is clicked inside an angular directive, angular will fail to render on the page specified by the href.
As a warning, this will be a long and in depth post, with detailed instructions on how to recreate the issue. I will provide a small proof of concept for the bug where angular1 and sennajs fail, and then provide an example of the exact same code of it working with react and sennajs.

Prerequisites: Use a fresh liferay portal bundle, and then use the theme generator to create a fresh theme. This will ensure that no other configuration issues exist.

Modify your portal_normal.ftl by adding the following lines to your head. This will provide the frameworks needed:

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.1/angular.min.js"></script>
<script src="https://cdn.jsdelivr.net/react/0.14.0-rc1/react.js"></script>
<script src="https://cdn.jsdelivr.net/react/0.14.0-rc1/react-dom.js"></script>
 <script src="${themeDisplay.getPathThemeJavaScript()}/angular/dist/app.min.js?t=${theme_timestamp}"></script>


Under the src/js folder in the theme, create a folder called 'angular'

create a file in angular called modules.js with the following contents:


angular.module('poc.controllers', []);
angular.module('poc.directives', []);
angular.module('poc.services', []);
angular.module('poc', ['poc.controllers', 'poc.directives', 'poc.services']);


Modify your gulpfile.js to add the angular build task:
NOTES:
  • I am using node 6.9.1 so I am using ES6 style variables, you may need to convert them to var if you are on an older version
  • React does not need a build process as we are using the native methods and not JSX
  • The angular build process is a bit much for this trivial POC, but originally I was attempting to build out a full application in angular, so I've based my POC off of that existing structure



let gulp = require('gulp');
let sourcemaps = require('gulp-sourcemaps');
let liferayThemeTasks = require('liferay-theme-tasks');
let util = require('gulp-util');
let concat = require('gulp-concat');
let ngAnnotate = require('gulp-ng-annotate');
let _ = require('lodash');

let angularMinTask = function (gulp) {
    gulp.src(['src/js/angular/*.js'])
        .pipe(sourcemaps.init())
        .pipe(concat('app.min.js'))
        .pipe(ngAnnotate())
        .pipe(sourcemaps.write())
        .pipe(gulp.dest('src/js/angular/dist'));
};

liferayThemeTasks.registerTasks({
	gulp: gulp,
    hookFn: function(gulp) {
        gulp.hook('before:build:src', function(callback) {
             angularMinTask(gulp);
            callback();
        });
});




Place the following divs in the main content section in your portal_normal:

<div id="testAngular"></div>
<div id="testReact"></div>


We are now set to begin testing. Place the following script at the bottom of your portal normal within the body. This script initializes a small angular directive consisting of a div and an anchor tag.


appDiv = document.createElement('div');
    appDiv.id = "myApp";
    appDiv.innerHTML = '<div class="my-bootstrap"></div>';
    document.getElementById('testAngular').appendChild(appDiv);

    angular.module('poc.directives').directive('myBootstrap', function () {
        return {
            restrict: 'AEC',
            template: '<div><a href="#">my link angular</a></div>'
        };
    });
    angular.bootstrap(angular.element(appDiv), ['poc']);



When the link is clicked, you should see the following error:
Error: [$compile:multidir] http://errors.angularjs.org/1.6.1/$compile/multidir?p0=myBootstrap&amp;p1=%20(m…3A%20poc.directives)&amp;p4=template&amp;p5=%3Cdiv%20class%3D%22my-bootstrap%22%3E


Which links to this angular error documentation:
https://docs.angularjs.org/error/$compile/multidir?p0=myBootstrap&p1=%20(module:%20poc.directives)&p2=myBootstrap&p3=%20(module:%20poc.directives)&p4=template&p5=%3Cdiv%20class%3D%22my-bootstrap%22%3E

My hypothesis is that angular is doing tracking behind the scenes on the elements, which does not play nicely with sennajs's actions. When a link is clicked, senna will tear out the whole body tag, destroy those elements, and begin re-rendering the body. It will then trigger the angular script, which will again try to bootstrap the angular directive on to the 'testAngular' dom node, which will have been replaced by senna with a new node. However, internally, angular is not aware of the changes to the DOM, and so when it sees a dom node with the same id as one that already have a directive on it it throws an error. One way I thought of solving it would be to remove the dom node that angular is rendered on.

Add the following lines to the top of the script for angular:

if (document.getElementById('myApp') != null){
        console.log('removing!');
        document.body.removeChild(document.getElementById('myApp'));
    }


However, it still throws the multiple directive error. I think that since angular was designed to be a SPA and senna was designed to be an SPA, getting them to work together is extremely difficult if not impossible. You can easily get around the issue by adding 'data-senna-off=true' to all of your anchor tags, but then you are doing a hard refresh on every redirect and cannot take advantage of what senna offers. We profiled the javascript with angular on the page and noticed that a new document was being created on the page whenever a link was clicked, further supporting our hypothesis that angular is not cleaning itself up. We tried to look for a way to make angular reset itself but were not able to find anything. Because of this error, none of the bindings or event listeners get setup correctly within angular.

By comparison, React works very well.
Remove the angular script tag and add the following script for react:

<script>
    var rootElement = React.createElement('div', {},
            React.createElement('a', {href: '#'}, 'my link react'));

    ReactDOM.render(rootElement, document.getElementById('testReact'));
</script>

React handles the page refresh's fine, and behaves exactly as expected.

Has anyone else encountered similar issues while trying to get angular to work? And if so, were you able to find any solutions?
thumbnail
Ben VonDerHaar, modified 7 Years ago.

RE: Liferay DXP: SennaJS and Angular 1 incompatible?

Junior Member Posts: 39 Join Date: 6/11/13 Recent Posts
I am the technical lead on the project Zak is working on and we would really appreciate input from anyone, either to confirm or disprove our suspicions. Our firm has recently begun moving forward with AngularJS 1.X aggressively and we would like to make this work in DXP if possible.

Best,
-Ben VonDerHaar
Michael El Khoury, modified 4 Years ago.

RE: Liferay DXP: SennaJS and Angular 1 incompatible?

New Member Posts: 9 Join Date: 9/18/19 Recent Posts
Hello Zak,

I think i'm late since it's been 2 years. but i'll answer anyways in case someone else is facing this issue.
You should try using the "data-senna-off=true" on your href that you don't want that senna capture. you can even configure senna in a way that it doesn't capture any redirection from your portlet. better check the documentation on SPA in the liferay website.
I think the reason that you are not having this problem with react is because react uses a virtual dom, so senna.js won't be able to catch it and manipulate it. 
Regards,Mike