I have a $http service that read a json file. I want to use modularization, but I don't know how to call my service in my controller and I don't know if my service is well written.
Here a jsFidlle : https://jsfiddle.net/aqbmdrvn/ .
Thanks!!!
/// <reference path="../../typings/angularjs/angular.d.ts" />
/// <reference path="../../typings/angularjs/angular-route.d.ts" />
/// <reference path="../app.ts" />
/// <reference path="servicePets.ts" />
"use strict";
module AnimalPlanet {
// pet interface
export interface IPet {
type: string;
name: string;
age: number;
color: string;
specialCare: boolean;
availableForAdoption: boolean;
ldAdoption: boolean;
history: string;
featured: boolean;
newest: boolean;
imageUrl: string;
}
export interface RootObject {
pets: IPet[];
}
// pet controller with ui-grid
export class petsCtrl implements RootObject {
pets: IPet[];
constructor(private $http: ng.IHttpService,public petsService, private $scope: any, uiGridConstants: any, filterldAdoption: any) {
$scope.pets = {};
// ui grid option
$scope.gridOptions = {
enableSorting: true,
enableFiltering: true,
paginationPageSizes: [5, 10, 15],
paginationPageSize: 5,
onRegisterApi: (gridApi) => {
$scope.gridApi = gridApi;
},
columnDefs: [
{
name: 'type',
cellTooltip: true,
headerTooltip: true
},
{
name: 'name',
cellTooltip: true,
headerTooltip: true
},
{
name: 'age',
// filters: [{
// condition: uiGridConstants.filter.GREATER_THAN,
// placeholder: 'greater than'
// }, {
// condition: uiGridConstants.filter.LESS_THAN,
// placeholder: 'less than'
// }
// ],
cellTooltip: true,
headerTooltip: true
},
{
name: 'color',
cellTooltip: true,
headerTooltip: true
},
{
name: 'specialCare',
cellTooltip: true,
headerTooltip: true
},
{
name: 'availableForAdoption',
cellTooltip: true,
headerTooltip: true
},
{
name: 'history',
cellTooltip: true,
headerTooltip: true
},
{
name: 'featured',
cellTooltip: true,
headerTooltip: true
},
{
name: 'newest',
cellTooltip: true,
headerTooltip: true
},
{
name: 'imageUrl',
cellTooltip: true,
headerTooltip: true,
enableFiltering: false,
enableHiding: false,
cellTemplate: "<img width=\"50px\" ng-src=\"{{grid.getCellValue(row, col)}}\" lazy-src>"
}
]
};
// read json using http service
this.$http.get('/app/pets/pets.json').success((data) => { // pune te rog asta intr-un serviciu
// fill ui grid using http service
$scope.filterPets = data;
var uiGridPets = [];
angular.forEach($scope.filterPets, (item) => {
if (item.ldAdoption) {
uiGridPets.push(item);
}
});
$scope.gridOptions.data = uiGridPets;
// filter for main page with 3 pets
$scope.pets = data;
$scope.quantity = 3;
var featuredPets = [];
var newestPets =[];
angular.forEach($scope.pets, (item) => {
if (item.featured) {
featuredPets.push(item);
}
if(item.newest){
newestPets.push(item);
}
});
$scope.featuredPets = featuredPets;
$scope.newestPets = newestPets;
});
$scope.fromService = petsService.weatherChange();
}
}
petsCtrl.$inject = ['$http', '$scope', 'uiGridConstants', 'petsService'];
app.controller("petsCtrl", petsCtrl);
}
/// <reference path="../../typings/angularjs/angular.d.ts" />
/// <reference path="../../typings/angularjs/angular-route.d.ts" />
/// <reference path="../app.ts" />
"use strict";
module AnimalPlanet {
export interface IPetsService {
http: ng.IHttpService;
uiGridConstants: any;
}
export class servicePets implements IPetsService {
http: ng.IHttpService;
uiGridConstants: any;
constructor( $scope:any , $http: ng.IHttpService, uiGridConstants: any )
{
// read json using http service
$scope.pets = {};
this.http = $http;
}
public get() {
this.http.get('/app/pets/pets.json').success((data) => { // pune te rog asta intr-un serviciu
// fill ui grid using http service
var filterPets = data;
var uiGridPets = [];
angular.forEach(filterPets, (item) => {
if (item.ldAdoption) {
uiGridPets.push(item);
}
});
var gridOptions.data = uiGridPets;
// filter for main page with 3 pets
var pets = data;
var quantity = 3;
var featuredPets = [];
var newestPets =[];
angular.forEach(pets, (item) => {
if (item.featured) {
featuredPets.push(item);
}
if(item.newest){
newestPets.push(item);
}
});
var featuredPets = featuredPets;
var newestPets = newestPets;
});
}
}
servicePets.$inject = ['$http', '$scope', 'uiGridConstants'];
app.service('servicePets', servicePets);
}
Here's what I use for boilerplate controller and factory:
Controller code:
declare var app: angular.IModule;
class MyController {
static $inject = ['myService'];
localData: IGetDataResult;
constructor(private myService: MyService) {
this.myService.getData('key1')
.then(result => {
this.localData = result.data;
});
}
}
app.controller('myController', MyController);
Factory code:
declare var app: angular.IModule;
interface IGetDataResult {
field1: string;
}
class MyService {
static $inject = ['$http'];
constructor(private $http: angular.IHttpService) {
}
getData(key: string): angular.IHttpPromise<IGetDataResult> {
return this.$http({
method: 'POST',
data: {
key: key
},
url: '/WebService/GetData'
});
}
}
function MyServiceFactory($rootElement) : MyService {
const inj = $rootElement.injector();
return inj.instantiate(MyService);
}
app.factory('myService', ['$rootElement', $rootElement => MyServiceFactory($rootElement)]);
Some explanations:
The controller is straightforward enough, but note the usage of the static injection variable $inject. The order of elements in that array must correspond to the order of parameters in the constructor function.
My factory is instantiated by a factory function (pardon the pun). Note that I am injecting a dependency on $rootElement. I discovered that if I ever want to use $location I must use the injector instantiated on that $rootElement.
I am using a strongly typed return value on the $http call. This helps a lot for the obvious reasons.
Note that everything is definitely typed, except for $rootElement. There is no matching type for it in angular.d.ts that includes the injector() function.
One more thing, typescript lends itself to the controllerAs methodology. Meaning that you don't need to inject $scope into your controller, and in your html you must prepend the controller alias (the controllerAs value) before every scope expression. When doing it that way, you put all your scope variables and functions as members on your controller.
Sometimes you do need to inject $scope if you want to use $watch or $on, but there are a few gotchas there, with regards to this closure, but this is a topic for another post.
One gotcha with ui-grid is that if you use a string value for your gridoptions.data member, it must also include the controller alias prefix.
To modularize your code, you want to refactor the $http call and its dependency injection out of the controller and into the service.
When you inject the service into the controller you need to account for the module "namespace". So, your $inject will look like: "AnimalPlanet.petsService". This is probably why you are having trouble invoking the service from your controller. It doens't look like it's wired properly.
I don't use $inject the way you are doing it. I follow this pattern:
// Dependencies for this controller.
static $inject = ["$q",
"$location",
"$window",
"$scope",
"$log",
"common",
"RL.SS.Portal.RLToday.Services.AroundTheIndustryService",
"RL.SS.Portal.RLToday.Services.LocationFilterService"];
That's inside the class definition itself. I don't know if that's necessarily more accurate, but you might find it useful.
In the controller's constructor, you don't need to (and probably should not) make the petservice reference public, but that's not really your problem, just a style thing.
Once you get the injection right, you can do this from your controler:
this.petsService.Get().then( () => { /* success */ }, () => { /* fail */});
HTH.
(this is my first attempt at answering a stack overflow question, so if anyone wants to advise me on how to do better, please feel free to do so).
Related
Can anyone give an example on using services with Angular 1.5 components?
I'm trying to inject a service in an Angular 1.5 component, but it doesn't work.
I have a login component like so:
class Login {
constructor($scope, $reactive, $state, myService) {
console.log(myService.somevariable); //doesn't work
}
}
// create a module
export default angular.module(name, [
angularMeteor
]).component(name, {
templateUrl: 'imports/ui/components/${name}/${name}.html',
controllerAs: name,
controller: Login
});
My service looks like this:
angular.module(name).service("myService", function () {
this.somevariable = 'somevalue';
});
I just cant seem to be able to get the service injected in the component.What am I doing wrong?
SOLUTION:
With sebenalern's help, I got it working.
I needed a service to validate an email address using a regular expression. I did it like this:
import angular from 'angular';
import angularMeteor from 'angular-meteor';
class Validator {
validateEmail(email) {
var re = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
}
const name = 'validator';
// create a module
export default angular.module(name, [
angularMeteor
])
.service("Validator", Validator);
I then injected the service like so:
import {name as Validator} from '../../../api/services/validator'
class Login {
constructor($scope, $reactive, $state, Validator) {
'ngInject';
this.$state = $state;
$reactive(this).attach($scope);
this.Validator = Validator;
}
login() {
if(this.Validator.validateEmail(this.credentials.email)) {
// email is valid.
}
}
}
const name = 'login';
export default angular.module(name, [
angularMeteor,
Validator
]).component(name, {
templateUrl: `imports/ui/components/${name}/${name}.html`,
controllerAs: name,
controller:Login
})
Hope this helps :)
So one problem I see is you should be using the keyword this inside the constructor
this.$scope = $scope;
Another thing it is probably easier to stay away from classes and use functions:
class Login {
constructor($scope, $reactive, $state, myService) {
console.log(myService.somevariable); //doesn't work
}
}
Becomes:
angular
.module('name')
.service('myService', myService);
function myService () {
this.somevariable = 'somevalue';
}
To me it seems a lot cleaner. Also another thing about ES6 classes is
ES6 Classes are not hoisted, which will break your code if you rely on hoisting
For more info see link.
Now here is the working code I came up with:
First we declare the module:
angular.module('name', []);
Next we register our service and then create the service definition:
angular
.module('name')
.service('myService', myService);
function myService () {
this.somevariable = 'somevalue';
}
Next we do the same procedure for our controller and also we inject $scope and our service into it.
angular
.module('name')
.controller('Login', Login);
function Login($scope, myService) {
$scope.someVar = myService.somevariable;
}
Last I registered our component:
angular
.module('name')
.component('my-html', {
templateUrl: 'my-html.html',
controller: Login
});
And that is it on the javascript side.
Here is my html code:
<!DOCTYPE html>
<html lang="en-us" ng-app='name'>
<head>
<script src="//code.angularjs.org/1.5.0-rc.1/angular.js"></script>
<script src="controller.js"></script>
</head>
<body >
<h ng-controller="Login">{{ someVar }}</h>
</body>
</html>
I hope this helps!!
Here is how I am doing it and it works well. It works fine with classes. I assume you are using TypeScript.
class AdminHomeService {
consignment: IConsignment;
get: () => IConsignment;
constructor() {
this.consignment = new Consignment();
this.consignment.id = 10;
this.consignment.customer = "Customer3";
this.consignment.customerList = [{ id: 1, name: "Customer1" }, { id: 2, name: "Customer2" }, { id: 3, name: "Customer3" }];
this.consignment.shipperList = [{ key: "1", value: "Shipper1" }, { key: "2", value: "Shipper2" }, { key: "3", value: "Shipper3" }];
this.consignment.consigneeList = [{ key: "1", value: "Consignee1" }, { key: "2", value: "Consignee2" }, { key: "3", value: "Consignee3" }];
this.consignment.billingList = [{ key: "1", value: "Billing1" }, { key: "2", value: "Billing2" }, { key: "3", value: "Billing3" }];
this.consignment.carrierList = [{ key: "1", value: "Carrier1" }, { key: "2", value: "Carrier2" }, { key: "3", value: "Carrier3" }];
this.get = () => {
return this.consignment;
}
}
}
class AdminHomeComponentController {
consignment: IConsignment;
selectedCustomer: any;
static $inject = ["adminHomeService"];
constructor(private adminHomeService: AdminHomeService) {
this.consignment = new Consignment();
this.consignment = this.adminHomeService.get();
this.selectedCustomer = {};
this.selectedCustomer.selected = { "name": this.consignment.customer };
}
customerAddClick(): void {
}
}
class AdminHomeComponent implements ng.IComponentOptions {
bindings: any;
controller: any;
templateUrl: string;
$routeConfig: angular.RouteDefinition[];
constructor() {
this.bindings = {
textBinding: "#",
dataBinding: "<",
functionBinding: "&"
};
this.controller = AdminHomeComponentController;
this.templateUrl = "templates/admin.home.html";
//this.$routeConfig = [
// { path: "/admin", name: "AdminHome", component: "adminHome", useAsDefault: true }
//];
}
}
angular.module("adminHome", [])
.component("adminHome", new AdminHomeComponent())
.service("adminHomeService", AdminHomeService);
This post helped me a lot: http://almerosteyn.com/2016/02/angular15-component-typescript
Factory:
app.factory('myGlobals', function ($http) {
function getCountries() {
var countries = [];
$http.get('http://whatever/v1/countries').then(function (response) {
countries = response.data;
});
return countries;
}
return {
getCountries: getCountries
}
});
Constant defined in an angular.module
.constant ('SomeSchema', {
type: 'object',
properties: {
host: {type: 'string', title: 'Host'},
ip: {type: 'string', title: 'IP'},
country: {
type: 'string',
title: 'Country',
enum: getCountries
},
status: {type: 'boolean', title: 'Status'}
},
required : [
"host","ip","country","status"
]
})
What should I do, so that I can use e.g. getCountries in the example above? (yes it is a constant, please ignore this issue..)
value is the shorthand for provider with predefined $get function, it can't be injected with dependencies. factory is the shorthand also, and it can be injected with dependencies. Use
.factory('SomeSchema', function (myGlobals) {
return { ... };
})
yes it is a constant, please ignore this issue..
It can't be ignored, because the distinctive feature of constants is that they can be used in config blocks, where myGlobals factory isn't available yet.
I think i have a scope problem with js. Please take a look to my code below.
This is my AngularJS example in es6. I compile the code to es5 with grunt browserify.
If i call my example i got the error:
TypeError: this.gatewayServiceGet is not a function
at ChainsDirective.loadChains [as chainsServiceLoadChains]
I check it and find out that this in loadChains is not the same this than in the constructor.
What can i do?
This is my app.js
'use strict';
import AppController from './appController.js';
import ChainsDirective from './components/chains/chains.directive.js';
import ChainsService from './components/chains/chains.service.js';
import GatewayService from './components/common/gateway/gateway.service.js';
angular
.module('SalesCockpit', ['ui.router', 'ui.grid'])
.config($stateProvider => {
$stateProvider
.state('chains', {
url: '/chains',
templateUrl: 'components/chains/chains.html'
})
.state('chainDetail', {
url: '/chain/{chainId:int}/detail',
templateUrl: 'components/chain-detail/chain-detail.html'
})
;
})
.controller('AppController', AppController)
.service('chainsService', ChainsService)
.service('gatewayService', GatewayService)
.directive('chains', ChainsDirective);
This is my chain directive
export default function ChainsDirective() {
class ChainsDirective {
/*#ngInject*/
constructor(chainsService, $state) {
this.chainsServiceLoadChains = chainsService.loadChains;
this.gridOptions = {
enableColumnMenus: false,
columnDefs: [
{
name: 'id',
visible: false
},
{
name: 'name',
displayName: 'Kette',
cellTemplate: '<div class="ui-grid-cell-contents"><a ng-click="grid.appScope.openDetail(row.entity.id)">{{row.entity.name}}</a></div>'
}
]
};
this.$stateGo = $state.go;
this.fetch();
}
/**
* #param int chainId
*/
openDetail(chainId) {
this.$stateGo('chainDetail', {chainId})
}
fetch() {
return this.chainsServiceLoadChains().then(data => {
this.gridOptions.data = data
})
}
}
return {
restrict: 'E',
template: '<div id="chains" ui-grid="gridOptions" external-scopes="$scope" class="grid"></div>',
controller: ChainsDirective,
controllerAs: 'chains'
}
}
This is my chain servie
export default class ChainsService {
/*#ngInject*/
constructor(gatewayService) {
this.gatewayServiceGet = gatewayService.get;
}
/**
* #returns Promise
*/
loadChains() {
return this.gatewayServiceGet('loadChains');
}
}
FWIW, this has nothing to do with ECMAScript 2015. JavaScript always worked that way.
The value of this depends on how the function is called. So if you call it as
this.chainsServiceLoadChains()
this inside chainsServiceLoadChains will refer to what is before the ., which is this that refers to the ChainsDirective instance.
One solution would be to bind the this value of the function to a specific value:
this.chainsServiceLoadChains = chainsService.loadChains.bind(chainsService);
Now it doesn't matter anymore how the function is called, this will always refer to chainsService.
Learn more about this:
MDN - this
How to access the correct `this` context inside a callback?
I have a ui-select field
{
key: 'data_id',
type: 'ui-select',
templateOptions: {
required: true,
label: 'Select label',
options: [],
valueProp: 'id',
labelProp: 'name'
},
controller: function($scope, DataService) {
DataService.getSelectData().then(function(response) {
$scope.to.options = response.data;
});
}
}
How can I access that inner controller in my unit tests and check that data loading for the select field actually works ?
UPDATE:
An example of a test could be as such:
var initializePageController = function() {
return $controller('PageCtrl', {
'$state': $state,
'$stateParams': $stateParams
});
};
var initializeSelectController = function(selectElement) {
return $controller(selectElement.controller, {
'$scope': $scope
});
};
Then test case looks like:
it('should be able to get list of data....', function() {
$scope.to = {};
var vm = initializePageController();
$httpBackend.expectGET(/\/api\/v1\/data...../).respond([
{id: 1, name: 'Data 1'},
{id: 2, name: 'Data 2'}
]);
initializeSelectController(vm.fields[1]);
$httpBackend.flush();
expect($scope.to.options.length).to.equal(2);
});
You could do it a few ways. One option would be to test the controller that contains this configuration. So, if you have the field configuration set to $scope.fields like so:
$scope.fields = [ { /* your field config you have above */ } ];
Then in your test you could do something like:
$controller($scope.fields[0].controller, { mockScope, mockDataService });
Then do your assertions.
I recently wrote some test for a type that uses ui-select. I actually create a formly-form and then run the tests there. I use the following helpers
function compileFormlyForm(){
var html = '<formly-form model="model" fields="fields"></formly-form>';
var element = compile(html)(scope, function (clonedElement) {
sandboxEl.html(clonedElement);
});
scope.$digest();
timeout.flush();
return element;
}
function getSelectController(fieldElement){
return fieldElement.find('.ui-select-container').controller('uiSelect');
}
function getSelectMultipleController(fieldElement){
return fieldElement.find('.ui-select-container').scope().$selectMultiple;
}
function triggerEntry(selectController, inputStr) {
selectController.search = inputStr;
scope.$digest();
try {
timeout.flush();
} catch(exception){
// there is no way to flush and not throw errors if there is nothing to flush.
}
}
// accepts either an element or a select controller
function triggerShowOptions(select){
var selectController = select;
if(angular.isElement(select)){
selectController = getSelectController(select);
}
selectController.activate();
scope.$digest();
}
An example of one of the tests
it('should call typeaheadMethod when the input value changes', function(){
scope.fields = [
{
key: 'selectOneThing',
type: 'singleSelect'
},
{
key: 'selectManyThings',
type: 'multipleSelect'
}
];
scope.model = {};
var formlyForm = compileFormlyForm();
var selects = formlyForm.find('.formly-field');
var singleSelectCtrl = getSelectController(selects.eq(0));
triggerEntry(singleSelectCtrl, 'woo');
expect(selectResourceManagerMock.searchAll.calls.count()).toEqual(1);
var multiSelectCtrl = getSelectController(selects.eq(1));
triggerEntry(multiSelectCtrl, 'woo');
expect(selectResourceManagerMock.searchAll.calls.count()).toEqual(2);
});
I'm making an Angular App and I'm starting to use some of the Kendo UI controls. I'm having some issues wiring up the AutoComplete control. I would like to use a factory that will return the list of "auto complete" values from my database.
I have iincluded the auto complete control and I'm trying to use the k-options attribute:
<input kendo-auto-complete ng-model="myFruit" k-options="FruitAutoComplete" />
In my controller the following hard coded list of fruits work:
$scope.FruitAutoComplete = {
dataTextField: 'Name',
dataSource:[
{ id: 1, Name: "Apples" },
{ id: 2, Name: "Oranges" }
]
}
When I move this over to use my factory I see it calling and returning the data from the factory but it never get bound to the screen.
$scope.FruitAutoComplete = {
dataTextField: 'Name',
dataSource: new kendo.data.DataSource({
transport: {
read: function () {
return FruitFactory.getYummyFruit($scope.myFruit);
}
}
})
}
I end up with the request never being fulfilled to the auto complete.
My factory is just returning an array of fruit [
my Fruit Factory Code:
getYummyFruit: function (val) {
return $http.get('api/getFruitList/' + val)
.then(function (res) {
var fruits= [];
angular.forEach(res.data, function (item) {
fruits.push(item);
});
return fruits;
});
}
Here is your solution
http://plnkr.co/edit/iOq2ikabdSgiTM3sqLxu?p=preview
For the sake of plnker I did not add $http (UPDATE - here is http://plnkr.co/edit/unfgG5?p=preview with $http)
UPDATE 2 - http://plnkr.co/edit/01Udw0sEWADY5Qz3BnPp?p=preview fixed problem as per #SpencerReport
The controller
$scope.FruitAutoCompleteFromFactory = {
dataTextField: 'Name',
dataSource: new kendo.data.DataSource({
transport: {
read: function (options) {
return FruitFactory.getYummyFruit(options)
}
}
})
}
The factory -
factory('FruitFactory', ['$http',
function($http) {
return {
getYummyFruit: function(options) {
return $http.get('myFruits.json').success(
function(results) {
options.success(results);
});
}
}
}