CSC/ECE 517 Spring 2015 E1526
E1526. Responsive web design for Expertiza
Strategy
We analyzed the three Expertiza pages that we need to modify for this project, as well as all of their dependencies, and tried to identify all the tasks that we need to complete for this project. It became clear to us that a large majority of this project will be based on AngularJS, while the small fraction will be related to Bootstrap. Therefore, our strategy was to jumpstart this project by having all 4 team members work together on adding AngularJS to the Login page. Once the Login page was completed, we split in two groups. One group worked on implementing AngularJS on Manage-course page, while the other group worked on introducing AngularJS to Manage-User page. The reasoning behind this strategy was that all of us working together on the Login page first allowed us to obtain some basic understanding of AngularJS and feel more comfortable using this impressively useful and powerful framework. We completed AngularJS constituents first by connecting some of the Rails controllers to AngularJS controllers and then, as a final step, we add Bootstrap. Our decision was based on the fact that Bootstrap does not require much work to be completed and it can be considered to be a pretty straight-forward element of the project.
Integration
This section represent the cornerstone of this project. It consciously explains how Angular and Rails can be integrated into a powerful system capable of providing solid control over both the backend and frontend development of your application.
Steps to interconnect Rails and AngularJS
These guidelines assume that you have a Rails project in place and that you are intending to add AngularJS to it now. Please follow the steps bellow:
1. In the Gemfile, ADD gem ‘bower-rails’ AND gem ‘angular-rails-templates’, then REMOVE ‘gem turbolinks'
2. run
bundle install
3. Create a Bowerfile. It is used by AngularJS and it can be thought of as the Gemfile that Rails uses.
4. In the Bowerfile, ADD asset ‘angular’ , asset ‘bootstrap-sass-official’, asset ‘angular-route’ AND asset ‘angular-boostrap’(for ui.bootstrap injection)
5. run
rake bower:install
. Bower installs dependencies in vender/assets/bower_components.
6. ADD the following content to the config/application.rb
config.assets.paths << Rails.root.join("vendor","assets","bower_components") config.assets.paths << Rails.root.join("vendor","assets","bower_components","bootstrap-sass-official","assets","fonts") config.assets.precompile << %r(.*.(?:eot|svg|ttf|woff|woff2)$)
7. REMOVE //= require turbolinks AND ADD
//= require angular/angular //= require angular-route/angular-route //= require angular-rails-templates //= require angular-bootstrap
to assets/javascripts/application.js
8. Rename assets/stylesheets/application.css to assets/stylesheets/application.css.scss.
9. ADD
@import "bootstrap-sass-official/assets/stylesheets/bootstrap-sprockets"; and @import "bootstrap-sass-official/assets/stylesheets/bootstrap";
to application.css.scss
Since we are did not get to build a new website using AngularJS from scratch, it was a good idea to retain the routing system of Rails rather than implement it in AngularJS.
So, we added a new tag div ng-app='MPApp'
into the app/views/layout/application.html.erb to include all pages into the scope of our AngularJS app, named ‘MPApp’.
Then, for the home page app/views/content_pages/view.html.erb, we added a tag to make this page controlled by ‘testCtrl’, which was defined in app.coffee.
AngularJS's point of view
1. Retrieving data from database Because it is not appropriate to let AngularJS to directly fetch data from the MySQL database, the only way we could load data was through a Rails controller. For example, it is possible to do a http get/post request(GET /users/fetch_10_users) from AngularJS controller using $http injection or jQuery AJAX, which calls a new method ‘def fetch_10_users’ in the users_controller, which execute some database query via accessing methods in the user model, and returns json formatted data to the AngularJS controller. After getting the returning data, the AngularJS controller can present the data to the screen.
2. Build custom directives Instead of using the original Rails page, we decided to replace some forms and components using AngularJS directives, which enabled us to do some live modifications to the pages without loading.
Rails' point of view
The initial problem was how to provide entries from the database to AngularJS. We decided to create methods in Rails controller which will listen to the incoming AngulaJS and use a select statement to fetch a couple of entries from the database. These entries are then stored in a hash, converted to json and passed to AngularJS controller. AngularJS will then send another query request to Rails for the next set of entries. This strategy greatly reduces the loading time, making the time needed for representing the page kept at the minimum.
The specifics
This section explains the very detailed actions and their results related to the 3 specific pages that were used to integrate AngularJS. Numerous snapshots are provided with the goal to make our approach very clear.
Login procedure:
‘Login’ button clicked → auth_controller#login → auth_controller#after_login(user) → if user.role != ‘student’ → tree_display#drill → tree_display#list
If session[:menu] is defined, that is, when a user is logging in, the page will render the content of variable session[:menu].
<ul class="nav navbar-nav"> <% if session[:menu] %> <%= render :partial => 'menu_items/suckerfish', :locals => { items: session[:menu].get_menu(0), level: 0 } %> <% end %> </ul> <ul class="nav navbar-nav"> <% if session[:menu] %> <%= render :partial => 'menu_items/suckerfish', :locals => { items: session[:menu].get_menu(0), level: 0 } %> <% end %> </ul>
For each menu item in the nav bar, it is a dropdown menu now. For different level of dropdown menu, we apply different CSS style to it.
<% level += 1 %> <% items.each do |item_id| item = session[:menu].get_item(item_id) selected = session[:menu].selected?(item.id) long_name = item.name.split(/\W/).collect{|word| word.capitalize}.join(' ') %> <% if level == 1 %> <li class="dropdown first-level"> <% elsif level == 2 %> <li class="dropdown-submenu"> <% else %> <li class="dropdown"> <% end %> <a href=<%= URI.encode("/menu/#{item.name}") %> <%= if item.children ' class="daddy"' end %>title='<%= long_name %>'><%= item.label.html_safe %></a> <% if item.children -%> <% if level == 1 %> <ul class="dropdown-menu multi-level"> <%= render :partial => 'menu_items/suckerfish', :locals => {:items => item.children, :level => level} %> </ul> <% else %> <ul class="dropdown-menu"> <%= render :partial => 'menu_items/suckerfish', :locals => {:items => item.children, :level => level} %> </ul> <% end %> <% end -%> </li> <% end -%>
Please also refer to app/assets/application.css.scss for further details.
Manage-course page
Design
After logging in, only the table frame is loaded into the page by rails. Then AngularJS takes over in the background and populate the content into the table. After all first-level content is populated into the table, in this case, all courses info belongs to the first level and their assignment is the second-level content.
After all first level content is loaded into the table, the Angular controller starts to initialize POST request for second level content.
In all, rendering the tree_display page is divided into 3 steps:
1. render the table frame,
2. render the first level table content.
3. fetch deeper level data.
In the past, these 3 steps are done before rendering the page, which takes about 10-20 seconds. But now, it takes only 2-3 seconds to finish the first 2 steps, and 2 more seconds for the 3rd step.
Moreover, the sorting and table record filtering is integrated into the table. No redirecting or re-rendering is needed for sorting or filtering, which was needed in the past.
Code details
Pass Rails controller params to view in tree_display_controller#list
angularParams = {} angularParams[:search] = @search angularParams[:show] = @show angularParams[:child_nodes] = @child_nodes angularParams[:sortvar] = @sortvar angularParams[:sortorder] = @sortorder angularParams[:user_id] = session[:user].id angularParams[:nodeType] = 'FolderNode' @angularParamsJSON = angularParams.to_json
Then the view passes to AngularJS
<div ng-controller='TreeCtrl'> <div ng-init="init('<%= @angularParamsJSON %>')"></div>
AngularJS handles it, after all basic elements are loaded onto the page. Then AngularJS initializes a POST request for retrieving more data to populate the table.
$scope.init = (value) -> $scope.angularParams = JSON.parse(value) $http.post('/tree_display/get_children_node_ng', { "angularParams": $scope.angularParams }) .success((data) -> $scope.tableContent = {} $scope.displayTableContent = {} $scope.lastLoadNum = {} $scope.typeList = [] console.log data for nodeType, outerNode of data $scope.tableContent[nodeType] = [] $scope.lastLoadNum[nodeType] = 0 $scope.displayTableContent[nodeType] = [] $scope.typeList.push nodeType # outerNode is the Assignments/Courses/Questionnaires for node in outerNode $scope.tableContent[nodeType].push node $scope.fetchCellContent(nodeType, node) $scope.getMoreContent(nodeType, 1) )
Fetch table cell content recursively and put them into a hash
$scope.fetchCellContent = (nodeType, node) -> $scope.newParams = {} $scope.newParams["sortvar"] = $scope.angularParams["sortvar"] $scope.newParams["sortorder"] = $scope.angularParams["sortorder"] $scope.newParams["search"] = $scope.angularParams["search"] $scope.newParams["show"] = $scope.angularParams["show"] $scope.newParams["user_id"] = $scope.angularParams["user_id"] key = node.name + "|" + node.directory $scope.newParams["key"] = key # console.log key if nodeType == 'Assignments' $scope.newParams["nodeType"] = 'AssignmentNode' else if nodeType == 'Courses' $scope.newParams["nodeType"] = 'CourseNode' else if nodeType == 'Questionnaires' $scope.newParams["nodeType"] = 'FolderNode' else $scope.newParams["nodeType"] = nodeType $scope.newParams["child_nodes"] = node.nodeinfo $http.post('/tree_display/get_children_node_2_ng', { "angularParams": $scope.newParams }) .success((data) -> if data.length > 0 for newNode in data if not $scope.allData[newNode.key] $scope.allData[newNode.key] = [] $scope.display[newNode.key] = false $scope.allData[newNode.key].push newNode $scope.fetchCellContent(newNode.type, newNode) )
After all data is fetched, the Rails view is able to presenting them onto the page!
<tbody ng-repeat="record in displayTableContent['Courses'] | filter:searchText | orderBy:predicate:reverse"> <tr ng-click="showCellContent(record.name, record.directory)" class="clickable-row"> <td>{{record.name}}</td> <td>{{record.directory}}</td> <td>{{record.creation_date}}</td> <td>{{record.updated_date}}</td> <td> </td> </tr> <tr class="no-cursor-tr no-color-tr" ng-if="display[record.name+'|'+record.directory] && allData[record.name+'|'+record.directory]"> <td colspan="3"> <table class="normal-table table table-bordered table-striped compact-table" ng-repeat="cell in cellContent"> <tbody> <tr class="no-cursor-tr no-color-tr"> <td><b>Name</b>: {{cell.name}}</td> </tr> <tr class="no-cursor-tr no-color-tr"> <td><b>Directory</b>: {{cell.directory}}</td> </tr> <tr class="no-cursor-tr no-color-tr"> <td><b>Creation Date</b>: {{cell.creation_date}}</td> </tr> <tr class="no-cursor-tr no-color-tr"> <td><b>Modified Date</b>: {{cell.updated_date}}</td> </tr> </tbody> </table> </td> </tr> </tbody>
Manage Users page
users