Some time ago I wrote articles about my "web-technology" tests. The first article covered AngularJS with JVx in action. It was a simple list view for contacts, had an Edit form and was based on AngularJS 1.4. The second article was about Oracle JET with JVx. The result was another list view with contacts, without Edit form.
Both solutions were more or less a demonstration for the automatic REST interface of JVx but not a real technology PoC.
The second article about Oracle JET was written in Feb 2016 and the first, about AngularJS, was written in July 2015 - long time ago. Currently, Angular 4 is available and Angular 5 is coming. Big version jumps with big API changes and many new features.
Last week, I thought it might a good idea to start a new evaluation of AngularJS. One week later, I think different... but one step after another.
My plan was the creation of a very simple application with some enhanced features, like routing, CRUD, styling, custom components, REST with authentication, Deployment. This was the base idea because I read really cool marketing articles about Angular and thought it might be easy to create a simple application with expected features.
So, what did I do to start?
The Angular tutorial was great for a jump start. You'll build a smart heroes application with a small dashboard. Sure, it's not a real world application but has everything on board and hey, the Tutorial will show how to integrate REST services. I thought it might be an easy task and won't last more than 1 day!
The tutorial has 7 chapters and the last one integrates REST. The chapter 1 was easy, because it was the instruction The second chapter contains the setup process which should be trivial, because I manually tried to create an AngularJS 4 application from scratch and it was super simple. But the setup for the tutorial wasn't easy because the creation of a new project from scratch contained too many files and the expected project layout (shown in the tutorial) was reduced to a bare minimum. It wasn't possible to delete all additional files because the project preview in the browser didn't work afterwards. So I decided to download the example archive.
This was the right decission because everything was pre-configured First lesson learned: Project configuration is not easy and woha, many different config files.
It was super easy to follow the tutorial and chapters 3, 4 were relative short and fast to do. Starting with chapter 5, the whole thing was starting to get complex because Service integration, Routing and HTTP(REST) were complex things. The chapters 5, 6 and 7 are very long and boring because you have to do so many things (hey, it's a tutorial and it's good as it is!).
No worries because I'm an "expert" and so I jumped to Chapter 6 and downloaded the finished example archive. The example was working without problems in my test environment but it was without support for HTTP/REST calls because I thought it might be a good idea to integrate the features based on the tutorial on my own.
So the next step was the integration of my own REST service instead of the dummy service in the tutorial. But first... I needed a REST service.
The creation of a web service was super easy because of our Low Code Development Platform - VisionX. It creates JVx based applications with all gimmicks. I did create a simple Heroes application with VisionX. The application has one screen:
Heroes app created with VisionX
The application was ready to use in 1 minute and had one database table:
Heroes table
The heroes table contains the columns: ID and NAME. Thanks to JVx, the REST service was ready-to-use. The request: http://localhost/services/rest/Heroes/HeroesWorkScreen/data/heroes
returned the JSON string with my test data:
[
{
"ID": 1,
"NAME": "Superman"
},
{
"ID": 4,
"NAME": "Rambo (John)"
},
{
"ID": 6,
"NAME": "Wolverine (X-Men)"
},
{
"ID": 2,
"NAME": "Batman"
},
{
"ID": 5,
"NAME": "Captain America"
},
{
"ID": 3,
"NAME": "ANT MAN"
}
]
So, my backend was ready.
The Next step was the integration into my Angular frontend. It was easy to find the right source file for the integration, because the hero-service was encapsulated. The first problem was that my REST service required authentication and the service implementation didn't use authentication. I had the same problem with missing authentication support in Angular 1.4 but found some useful things on stackoverflow. With Angular 4 everything has changed because you code Typescript and the API is different to 1.4, so my old solution didn't work. I tried to find something in the Internet but had a lot of problems because most help was for Angular 2. And I found so many github issues for http authentication and most issues had tons of comments... frustrating and not effective.
I tried to search in the official Angular documentation and found some hints about HttpClient in the online guide. But I didn't know HttpClient because my example was created with HttpModule. Not helpful that the example from the tutorial is different to the official documentation.... or.... I didn't read the details.... or.... I'm a bad developer.... or (and this is a fact) my Javascript/Typescript know-how is not enough. But anyway, it was not a standard task to add authentication to my hero-service.
I'm not 100% sure how I solved the problem, but I guess I read the API doc about http and an information about the new Angular2 HttpModule (means that HttpModule is old and HttpClient is new?). I googled around for sime time and tried a lot of hints, but it was painful. So much documentation about such a simple problem and no concreate solution for the http authentication problem.
Here's my solution:
export class HeroService
{
private heroesUrl
: string
;
private options
: RequestOptions
;
private headers
: Headers
;
constructor
(private http
: Http
)
{
this.
headers = new Headers
();
this.
headers.
append('Accept', 'application/json')
this.
headers.
append('Content-Type', 'application/json');
this.
headers.
append('Authorization', 'Basic ' + btoa
("admin:admin"));
this.
options = new RequestOptions
({ headers
: this.
headers, withCredentials
: true });
this.heroesUrl = 'http://localhost/services/rest/Heroes/HeroesWorkScreen/data/heroes';
}
getHeroes(): Promise<Hero[]> {
return this.http.get(this.heroesUrl, this.options)
.toPromise()
.then(response => response.json() as Hero[])
.catch(this.handleError);
}
}
I know that username and admin are hard-coded but it's a simple test application without login. It's not really a problem to add a login form if needed and to replace the hard-coded values with variables.
After I solved the authentication problem, and everything was working in my test environment, I tried to continue with tutorial chapter 7 and tried to add new features like Adding new heroes, Deleting heroes. This was really straight forward and worked without problems, even with authentication.
Yuhu. After two days I had a working Angular 4 application which reads data from a REST service and offers CRUD. A simple routing was available and I had custom components, styling and was happy. I thought the next step should be a test deployment. Sounds easy and I thought it couldn't be a problem... but... deployment hell.
Why is Angular deployment so complex?
The documentation has a lot of information about deployment. The simple deployment is really straight-forward because it's enough to copy/paste your development environment. But this is not recommended for production environments because of performance and many other things. So, i tried to deploy a production ready application and not my development environment... And the nightmare started.
I found a super simple deployment guide for Angular 4. First problem: didn't work for my example project because:
Unable to find @angular/cli in devDependencies
Please take the following steps to avoid issues:
"npm install --save-dev @angular/cli@latest"
After installing the missing module, the next problem:
Cannot read property 'config' of null
TypeError: Cannot read property 'config' of null
Also no problem because google found that my project needs an additional config file:
.angular-cli.json
I found this file in my demo Angular application, created from scratch. I tried to edit and adapt the file for my heroes application and the next problem came up:
ENOENT: no such file or directory, stat '...\angular-tour-of-heroes\src\tsconfig.app.json'
No problem, same solution as before (copy/paste/change).
Hurray, the command:
ng build -prod
was successful!
The dist folder contained my production ready application with 6 files! Great.
The deployment to my Tomcat application server was also super easy, because VisionX is able to create war files with a single mouse click. I changed the created war file and put the built Angular files in the root directory and deployed the war file.
No surprise, the application didn't work because some javascript files weren't found. The developer console of Chrome browser was a big help!
So, what should I do?
No... not Google. I read the development guide and found the solution in section Load npm package files from the web (SystemJS).
So, I changed my deployment and replaced node_modules in my index.html with https://unpkg.com/. This solved one problem but the developer console showed more 404 errors. I had to remove following:
<script>System.import('main.js').catch(function(err){ console.error(err); });
</script>
and
<link rel="stylesheet" href="styles.css">
I also had to replace the node_modules path in systemjs.config.js with https://unpkg.com/. Oh, and the file was missing in dist folder. I decided to follow the deployment guide and created a file with the name systemjs.config.server.js. If you do this, the index.html needs a changed mapping.
I didn't replace the values by hand. Here's my ant build file:
<project name="AngularJS" default="start.complete">
<!--
*****************************************************************
* information
*****************************************************************
-->
<description>Angular JS deployment</description>
<!--
*****************************************************************
* tasks
*****************************************************************
-->
<target name="start.complete" description="Prepares production files">
<property name="heroes" location="D:/dev/other/angularjs/angular-tour-of-heroes" />
<property name="dist" location="${heroes}/dist" />
<replace file="${dist}/index.html" encoding="UTF-8">
<replacetoken><![CDATA[<link rel="stylesheet" href="styles.css">]]></replacetoken>
<replacevalue><![CDATA[]]></replacevalue>
</replace>
<replace file="${dist}/index.html" encoding="UTF-8">
<replacetoken><![CDATA[<script>System.import('main.js').catch(function(err){ console.error(err); });</script>]]></replacetoken>
<replacevalue><![CDATA[]]></replacevalue>
</replace>
<replace file="${dist}/index.html" encoding="UTF-8">
<replacetoken><![CDATA[node_modules/]]></replacetoken>
<replacevalue><![CDATA[https://unpkg.com/]]></replacevalue>
</replace>
<replace file="${dist}/index.html" encoding="UTF-8">
<replacetoken><![CDATA[<script src="systemjs.config.js"></script>]]></replacetoken>
<replacevalue><![CDATA[<script src="systemjs.config.server.js"></script>]]></replacevalue>
</replace>
<copy file="${heroes}/src/systemjs.config.server.js" tofile="${dist}/systemjs.config.server.js" />
</target>
</project>
With above changes, it was possible to use my application with my tomcat application server. But I had two more problems!
First, my REST service URL was wrong because it was set for my dev environment.
Second, the browser reload of URLs didn't work because the application server returned 404 for e.g. https://cloud.sibvisions.com/heroes/heroes/3
Two more problems, omg.
But clear, the REST service URL can't be the same in the prod environment. I saw that Angular comes with support for environments and thought this would solve my first problem.... No way!
The problem was that the environment integration worked for build time, but not for development. It wasn't possible to import the environment constant in my Typescript files. It wasn't possible beause the file wasn't routed correctly and I didn't find a solution for this problem. The dev server returned 404 for the access to ../environments/environment.js. I tried to find a solution for more than one day, but gave up. I found out that the config files of an Angular application are evil. You have so many options and have absolutely no idea what's right. This is a common problem if you search the internet and it's a good idea to use a pre-created configuration. But the configuration of the heroes application was different to a newly created configuration. So I reverted all my environment changes and decided to follow the official documentation. This code is the clue:
if (!/localhost/.test(document.location.host)) {
enableProdMode();
}
It doesn't read the value from the environment constant, as recommended here. Not sure if this was recommended in Angular 2 and not for Angular 4, but the support is still built-in. I found so many horrible solutions for the environment problem, but no simple and easy one.
I solved the problem with my heroes-service and static properties. Yep, static field properties. It's that easy!
Here's a snippet from my service:
export class HeroService
{
private static heroesUrl
: string
;
public static initMode
(prodMode
: boolean
): void {
if (prodMode
) {
enableProdMode
();
HeroService.heroesUrl = 'https://cloud.sibvisions.com/heroes/services/rest/Heroes/HeroesWorkScreen/data/heroes';
}
else {
HeroService.heroesUrl = 'http://localhost/services/rest/Heroes/HeroesWorkScreen/data/heroes';
}
}
...
I used the recommended environment detection in my main.ts:
HeroService.initMode(!/localhost/.test(document.location.host));
This solved my environment problem completely. I'm not sure what's the best solution and I would recommend the built-in environment solution, but there should be a working tutorial or example for development and deployment.
Second lesson learned: Deployment is not easy with Angular if you want to make it right. The documentation is not clear and it's complex!!!
Back to my second problem: browser reload!
This is documented here. Clear, isn't it?
So, no solution for my Apache Tomcat installation. Rewrite rules are supported but why so tricky? I won't configure routing in my application and in the application server. This should simply work.
I tried to find a simple solution and created a simple Filter, because too much configuration was not what I want!
My filter:
public void doFilter(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException
{
String sURI = ((HttpServletRequest)pRequest).getRequestURI();
if (StringUtil.isEmpty(FileUtil.getExtension(sURI)))
{
RequestDispatcher reqdis = pRequest.getRequestDispatcher(sIndex);
reqdis.forward(pRequest, pResponse);
}
else
{
pChain.doFilter(pRequest, pResponse);
}
}
Configration (web.xml):
<filter>
<filter-name>IndexHtmlFilter</filter-name>
<filter-class>demo.angular.IndexHtmlFilter</filter-class>
<init-param>
<param-name>index</param-name>
<param-value>/app/index.html</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>IndexHtmlFilter</filter-name>
<url-pattern>/app/*</url-pattern>
</filter-mapping>
Not my best code, but works.... if you put the application in a seperate folder... e.g. The REST servic call:
//souldn't route to index.html
https://cloud.sibvisions.com/heroes/services/rest/Heroes/HeroesWorkScreen/data/heroes
//should route to index.html
https://cloud.sibvisions.com/heroes/heroes/2
So it was easier to move the application into a sub directory: /app
https://cloud.sibvisions.com/heroes/app/heroes/2
and everything starting with /app/* will be routed to /app/index.html if necessary.
This configuration and the filter solved my second problem and hurray, my application was working without any problems!
Oh, in order to serve my application from the /app directory, I had to set the base href in index.html:
ng build -prod --base-href /heroes/app/
Here's the application: https://cloud.sibvisions.com/heroes/app/
(Please don't delete my default heroes because there's no "restore defaults" implemented)
What's next?
I wasn't happy with default features of tutorial app because I missed an export feature. So I decided to add a Report which contains all my heroes. The REST service was ready in one minute because VisionX has Reporting support. But how to download/save the report?
I had no example and google returned too many answers for Angular 2 and one for Angular 4. I followed the instructions and had many problems because the saveAs method wasn't found... So I had the next big problem!
Every new feature was a big problem for me
This feature was horrible because I had absolutely no plan about angular modules. I found several save-file modules and tried to install one by one. No module was working as documented. Not sure if the problem was because of my configuration or because of Angular 4. Finally I found one solution but I had to change my system.config.js manually and add:
'file-saver': 'npm:file-saver'
to the system loader and
'file-saver': {
format: 'global',
main: 'FileSaver.js',
defaultExtension: 'js'
}
to the packages.
I don't know why the default module installation didn't work but it did not!
After everything worked in my dev environment, I tried the same application in my prod environment and... error.
The FileSaver.js was not found???
Absolutely unclear, but I solved the problem dirty (or not) in my index.html. Simply added
<script src="node_modules/file-saver/FileSaver.min.js"></script>
and it was find in dev and prod environment.
... I thought.
One more problem: Download worked but the content was damaged!
The problem was related to the content-type or maybe the encoding?
Recommended (Solution 1, Solution 2) solution:
this.http.post(HeroService.heroesUrlAction + 'createListReportHeroes', JSON.stringify([null, null]))
.toPromise()
.then(response => {
var blob = new Blob([response._body], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
saveAs(blob, 'report.xlsx');
})
.catch(this.handleError);
This wasn't working because something was missing:
var optCopy
= new RequestOptions
({ headers
: this.
headers, withCredentials
: true, responseType
: ResponseContentType.
Blob });+
this.http.post(HeroService.heroesUrlAction + 'createListReportHeroes', JSON.stringify([null, null]), optCopy)
.toPromise()
.then(response => {
var blob = new Blob([response.blob()], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
saveAs(blob, 'report.xlsx');
})
.catch(this.handleError);
Clear, the ResponseContentType.Blob has to be set!!!
And if you set this type, the response._body throws a warning? Use response.blob() instead!
It took me one hour to remove this warning!
Next?
I'm done!
The whole application development made me 1 year older because of frustration. I know that I'm not an Angular expert and I didn't read everything in the official documentation or guide/tutorial, but the whole development was a torture - sure, the result is great!
Too many different solutions for simple problems: deployment, downloading a file, configuration.
The documentation is awesome but too complex and contains no simple use cases. A simple deployment would be awesome and the problem with environments was horrible. Angular is powerful, no doubt! The configuration and deployment aren't straight forward. Also the mechanism behind the module system was not always working, at least not for me.
I love using custom components and the styling integration is great. The routing configuration is error-prone without an IDE or specific tools. My application didn't use all available features like testing, linting, ...
Finally
The application is available as JVx application with Vaadin UI here.
Login with admin as username and password.
The Angular 4 application.