Learning Angular and Rails with Tuts Plus course.
First install Protractor
npm install protractor --save-dev
Use Protractor bin to install or update Selenium web driver
./node_modules/protractor/bin/webdriver-manager update
Configuration
cp ./node_modules/protractor/example/conf.js test/e2e.conf.js
Start web driver
./node_modules/protractor/bin/webdriver-manager start
Open another tab and run tests
./node_modules/protractor/bin/protractor test/e2e.conf.js
Edit Gemfile
add a development section
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug'
# Access an IRB console on exception pages or by using <%= console %> in views
gem 'web-console', '~> 2.0'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'spring'
# Testing
gem 'rspec-rails'
gem 'capybara'
gem 'selenium-webdriver'
end
Install new dependencies
bundle
Initialize rspec
rails g rspec:install
Run tests
rspec spec/features/navigation_spec.rb
ng-repeat-start
and ng-repeat-end
can be used to prolong the scope of ng-repeat
iterator in a template.
Example
By default, piping the output of a repeater through filter
searches through ALL attributes
<li ng-repeat-start="edge in edges | filter:filterBy.search"">
```
To only search a single attribute, for example `name`
```html
<li ng-repeat-start="edge in edges | filter: {name: filterBy.search}">
Custom filter example and unit test.
To have angular populate a select box with options. For example, given that categories
is in scope:
<select name="searchByCategory" id="searchByCategory" class="form-control"
ng-model="filterBy.category"
ng-options="c.name for c in categories">
</select>
Use Angular's http mock to unit test components that make a ajax requests.
To use it, inject $httpBackend
into controller unit tests. Example
When working with mock http, its a good idea to make sure there are no leftover pending requests.
Use afterEach
to verify verifyNoOutstandingExpectation
and verifyNoOutstandingRequest
.
To simulate an http request in a test
http.expectGET('/api/edges');
http.flush();
So that angular front end app can make /api calls to get edges.
First add api namespace to routes
Add a controller and controller test
Generate the models
rails g model edge name description:text category:references
rails g model requirement name value mode
rails g model category name
Setup app and test databases
rake db:migrate
rake db:test:prepare
Use Active Model Serializer to generate JSON representation of models.
Add it to gemfile, then run bundle
Generate
rails g serializer edge
rails g serializer requirement
rails g serializer category
Edit the generated {model}_serializer.rb
files to specify attributes.
If any changes are made to {timestamp}_create_{model}.rb
files to modify the database schema, run
rake db:drop
rake db:migrate
rake db:test:prepare
By default, rails active model serializer will generate root element. But angular app expects a list of results without a root element.
To fix this, specify root: false
in rails controller
module Api
class EdgesController < ApplicationController
def index
render json: Edge.all, root: false
end
end
end
Having trouble with rails active model serializer not following edge relationships and generating expected json. To not get stuck, use express back end
cd sw-express
DEBUG=myapp ./bin/www
Example view and controller
To activate Angular form validations, form must have a name AND must disable HTML5 form validation. The form name will appear as a property on $scope, which is then available in the controller.
<form class="form" name="loginForm" novalidate>
...
</form>
To have Angular control the form submission, use ng-submit
directive
<form class="form" name="loginForm" novalidate ng-submit="login()">
...
</form>
To check if form is valid in a controller
$scope.login = function() {
if ($scope.loginForm.$valid) {
// do something with form data...
}
}
Optionally, can choose to display error messages to user only when form is submitted.
To disable submit form button until form is valid
<button class="btn btn-primary" type="submit" ng-disabled="loginForm.$invalid">Log In</button>
Preferred approach is to display error messages on blur, i.e. when field loses focus. This requires a custom directive.
To use angular models in a directive, use require: 'ngModel'. This makes the model controller available as fourth argument in the
link` function.
See Angular Directive doc for more details, specificlly, "Creating Directives that Communicate".
link
function in directive allows us to listen to events such as blur
.
camelCase is used javascript
angular.module('swFrontApp')
.directive('cuFocus', function () {
return {
restrict: 'A',
require: 'ngModel',
// rest of logic goes here...
};
});
But snake-case is used in html
<input id="userEmail" type="email" name="email" class="form-control" ng-model="user.email" required cu-focus/>
Steps:
- Client sends credentials (such as email or username and password) to server, for example, via login form.
- Server checks if credentials are valid, generate unique token, saves it (eg: in a database), and returns token to client
- Client saves this token somewhere (cookie or local storage)
- Client includes this token on every subsequent request to server
- Server checks for token on every resource that requires authentication
Can configure an interceptor to intervene in all ajax requests, and modify request or response, success or error cases.
Interceptor is implemeted as a factory
. For example, to check all response errors for 401 and redirect user to login page:
.factory('myCustomHttpInterceptor', function($q, $location) {
return {
'response': function(response) {
return response;
},
'responseError': function(rejection) {
if (rejection.status === 401 && $location.url() !== '/login') {
$location.url('/login');
} else {
return $q.reject(rejection);
}
}
};
```
Can also use interceptor to modify every request, for example, suppose an auth_token is persisted in local storage,
and would like to include it as http header for every ajax request
```javascript
.factory('myCustomHttpInterceptor', function($q, $location) {
return {
'request': function(config) {
config.headers = config.headers || {};
if (localStorage.auth_token) {
config.headers.token = localStorage.auth_token;
}
return config;
}
};
To make your custom http interceptor actually be used by angular, use $httpProvider.
This would go somewhere in app.js
where main Angular application (routes etc) is configured.
.config(function($httpProvider) {
$httpProvider.interceptors.push('myCustomHttpInterceptor');
})
To create a new resource using ngResource
<!-- edges.html -->
<form name="newEdgeForm" ng-submit="createEdge()">
<input type="text" name="name" class="form-control" ng-model="newEdge.name" />
<textarea name="description" class="form-control" ng-model="newEdge.description"></textarea>
<!-- remainder of newEdge bound input fields... -->
</form>
// services/edgeresource.js
angular.module('swFrontApp')
.factory('EdgeResource', function ($resource) {
return $resource('/api/edges');
});
// controllers/edge.js
angular.module('swFrontApp')
.controller('EdgesCtrl', function ($scope, EdgeResource) {
$scope.newEdge = new EdgeResource;
});
To delete a resource
var deleteEdge = function(edge) {
edge.$delete();
}
But this only works, given that resource is defined as follows, specifying what object attribute to use for id, for example to use name as id
angular.module('swFrontApp')
.factory('EdgeResource', function ($resource) {
return $resource('/api/edges/:id', {id: '@name'});
});
Angular controllers tend to get big, means they're doing too much. Look at the html view and determine if it can be split up into multiple templates and controllers, each with a single responsibility.