OpenProject is a hybrid application consisting of a Ruby on Rails monolith with an Angular application serving specific modules of the OpenProject frontend. Strategically, the application is shifting towards a single-page application. However, many of the views are still generated largely by Rails and only extended by Angular components.
All requests to the application are still responded by Rails. In some of the responses, only the root Angular component is rendered to bootstrap the Angular frontend. On these pages, UI-Router for Angular parses the URL to determine what module/frontend route to load and show.
In the following, we’ll take a look at the two main ways the Rails templates will interact or trigger the Angular frontend.
Exemplary frontend view request
Let’s take a look at how the request to
/projects/identifier/work_packages would be handled by Rails and Angular (excluding any external actual HTTP requests to the web server)
- Rails receives the request and according to its
config/routes.rb, will handle the request with the WorkPackagesController#index action.
- This controller responds with an index template that only renders some details but otherwise, will output the
<openproject-base>Angular root component that is defined in the
Rails angular layout.
- The rendered response is returned to the Browser and Angular is initialized globally once in
- As the
<openproject-base>component contains a ui-router
[ui-ref]directive, the ui-router will start parsing the URL and looks for a route definition that matches. It will end up matching
root.work-packagesdefined in the work packages’ module routes file.
- From there, the flow is as with a single-page application. The router mounts that component and the Angular frontend will use the APIv3 to fetch and render the application table.
This will result in a page on which the majority of the content has been rendered by Angular. Only the toolbar, basic page structure, and upper side menu has been rendered by Rails.
This approach has the significant disadvantage to go through the entire Rails stack first to output a response that is mostly irrelevant for the Angular application, and both systems (Rails and Angular) need a somewhat duplicated routing information. The long-term goal is to move to a single-page application and avoid the first two steps.
Exemplary Rails view request augmented by Angular
A response that is fully controlled by Rails but extended by some Angular components in the frontend might look as follows. Let’s take a look at the request to edit a type’s form configuration
The rendered response is returned to the Browser and Angular is initialized globally once in
A global service, the
DynamicBootstrapper, looks for eligible components to bootstrap in the rendered template and forces the global angular application to bootstrap this component. This may result in many dom-separated components in the page to be bootstrapped by Angular for dynamic content.
This triggers the
FormConfigurationComponentto be initialized and allows the application to include a highly dynamic component (drag & drop organization of attributes) to be used on an admin form that otherwise has no connection to Angular.
Evolution of the application
In early 2019, the rest of AngularJS code was removed and the frontend switched to the Angular CLI with Ahead-of-Time compilation (AOT).