As far as I now, services need to be provided and injected, meaning each service has to be placed inside the constructor eg.:
constructor (private a: AService, private B: BService) {}
In my situation, I have a bunch of services (all implementing the same interface) I would like to hold in an array. How can I achieve this without a lot of redundancy (since I already have to state each twice for providing and injecting - right?)?
Regular classes instead of injectable services would not work for me because each of the services might need to use other services like HTTP which again have to be injected.
With a simple helper array, let's call it "registry" and a dedicated array provider, we can make all registered instances available. Here a quick example:
export class Examples extends Array<ExampleService> {}
const examplesRegistry = [ExampleServiceA, ExampleServiceB];
export function buildExampleProviders() {
return [
...examplesRegistry,
{
provide: Examples,
useFactory: (...args) => new Examples(...args),
deps: examplesRegistry,
},
];
}
As demonstrated below, we can still inject single instances (like protected exampleA: ExampleServiceA) or all of them with protected examples: Examples)
full example + usage
/// example.service.ts
import { Injectable } from '#angular/core';
#Injectable()
export abstract class ExampleService {
someProperty = Math.random();
abstract name: string;
}
#Injectable()
export class ExampleServiceA extends ExampleService {
override name = 'a';
}
#Injectable()
export class ExampleServiceB extends ExampleService {
override name = 'b';
}
/// examples.ts
export class Examples extends Array<ExampleService> {}
const examplesRegistry = [ExampleServiceA, ExampleServiceB];
export function buildExampleProviders() {
return [
...examplesRegistry,
{
provide: Examples,
useFactory: (...args) => new Examples(...args),
deps: examplesRegistry,
},
];
}
/// app.component.ts
import { Component } from '#angular/core';
import { ExampleServiceA } from './example.service';
import { buildExampleProviders, Examples } from './examples';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [buildExampleProviders()],
})
export class AppComponent {
constructor(
protected examples: Examples,
protected exampleA: ExampleServiceA
) {}
}
<!-- app.component.html -->
<pre>
<ng-container *ngFor="let ex of examples; let i = index">
examples[{{i}}]:
.name: {{ ex.name }}
.someProperty: {{ ex.someProperty }}
.constructor.name: {{ ex.constructor.name }}
</ng-container>
exampleA:
.name: {{ exampleA.name }}
.someProperty: {{ exampleA.someProperty }}
.constructor.name: {{ exampleA.constructor.name }}
</pre>
You can also try it out here:
https://stackblitz.com/edit/angular-ivy-3kaagr?file=src/app/examples.ts
Related
I'm trying to create a service to share the data between two components. I injected the service into root module to make it accessible throughout the application by doing DI into the root module provider. My code looks roughly like this.
Service
#Injectable(){
export class ForumService{
forum: any;
setForum(object){
this.forum = object;
}
getForum(){
return this.forum;
}
}
Root Module
.......
import { ForumService } from 'forumservice';
.......
#NgModule({
declarations: [.....],
imports: [.....],
providers: [....., ForumService],
bootstrap: [AppComponent]
})
export class AppModule{}
Component One
//A bunch of import statements
import { ForumService } from 'forumservice'; //Without this Angular throws a compilation error
#Component({
selector: 'app-general-discussion',
templateUrl: './general-discussion.component.html',
styleUrls: ['./general-discussion.component.css'],
providers: [GeneralDiscussionService] //Not injecting ForumService again
})
export class GeneralDiscussionComponent implements OnInit{
constructor(private forumService: ForumService){}
ngOnInit(){
helperFunction();
}
helperFunction(){
//Get data from backend and set it to the ForumService
this.forumService.forum = data;
console.log(this.forumService.forum); //prints the data, not undefined
}
}
Component Two
//A bunch of import statements
import { ForumService } from 'forumservice'; //Without this Angular throws a compilation error
#Component({
selector: 'app-forum',
templateUrl: './forum.component.html',
styleUrls: ['./forum.component.css'],
providers: []
})
export class ForumComponent implements OnInit {
forumData: any;
constructor(private forumService: ForumService){}
ngOnInit(){
this.forumData = this.forumService.forum; // returns undefined
}
}
Once I navigate from Component One to Component Two I'm expecting "This is a string". However I get undefined. Is it because of the import statements in the component? If I remove that I see a compilation error saying that ForumService is not found.
Instead of using getter and setter, use the object (not primitibe such as string) directly In your components.
Your service
#Injectable(){
export class ForumService{
forum:any = {name:string};
}
Component one
export class GeneralDiscussionComponent implements OnInit{
constructor(private forumService: ForumService){}
ngOnInit(){
this.forumService.forum.name="This is a string";
}
}
Component two
export class ForumComponent implements OnInit {
// forumTitle: string; // do not need this anymore
forum:any; // use the forum.name property in your html
constructor(private forumService: ForumService){}
ngOnInit(){
this.forum = this.forumService.forum; // use the
}
}
I know encapsulating is preferable, and with your current code you are probably encountering some timing problems. But when working with shared data in a service you can two-way bind the variable like above, and your components will be in sync.
EDIT:
Also an important notice, the variable you want to sync between components needs to be an object. instead of forumTitle:string, make it forumTitle:any = {subject:string} or something similar.
Otherwise you need to make your components as listeners for data when data changes in your service.
I'd use BehaviorSubject in this case, should be something like that:
#Injectable(){
export class ForumService{
private _forum: BehaviorSubject<any> = new BehaviorSubject<any>(null);
public forum: Observable<any> = this._forum.asObservable();
setForum(object){
this._forum.next(object);
}
}
Then just bind it in template with async pipe: {{forumService.forum|async}} or subscribe to it.
I have a component in angular 4 that is called three times. In template metadata I have a div with a directive with some bindings like this.
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="dataChart">`
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
cOptions:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.cOptions = {};
this.cOptions = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
//this.report.opt is binded to a component when is instantiated.
//this.gServ.objectMerge is a function that merge the two objects
}
}
this.cOptions change for every instance of the component, then in the directive I have this:
import { Directive, ElementRef, HostListener, Input, OnInit } from '#angular/core';
#Directive({
selector: '[gDirective]'
})
export class SGDirective implements OnInit {
public _element: any;
#Input() public cOptions: string;
constructor(public element: ElementRef) {
this._element = this.element.nativeElement;
}
ngOnInit() {
console.log(this.cOptions);
}
}
The problem is that console.log(this.cOptions); always print the same object, even when component set cOptions with diferent values in ngOnInit method of the compnent.
Do you have some idea what is wrong?
Your component property binding [cOptions]="dataChart" doesn't look good, reason being your dataChart is not even defined. it should be like [DIRECTIVE_PROPERTY]="COMPONENT_PROPERTY" and your COMPONENT_PROPERTY is not even defined in SGComponent component class.
Your component class should be something like this:
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="Options">`
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
Options:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.Options = {};
this.Options = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
}
}
#Ashwani points out a valid problem with your code. The way your template is wiring things up, nothing will ever be passed to the SGDirective input.
Another potential problem you could be running into has to do with the gServ code. If gServ is a singleton (which is probably the case) and it is returning the same object to each of the SGComponents, then all the SGDirectives will have the same value. A simple way to test this is to put {{Options | json}} in the SGComponent template.
To create a new instance of the gServ service for each SGComponent you can add a providers array to the #Component metadata. It would look like this:
import {gServ} from '../gServ.service';
#Component({
selector: 'sr-comp',
template: `{{Options | json}}<div gDirective [cOptions]="Options"></div>`
providers: [gServ],
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
Options:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.Options = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
}
}
You have probably the same return/value at this.gServ.objectMerge) (you can test it wihtout calling the service, and passing each one one different objet make by you)
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="dataChart">`
})
export class SGComponent implements OnInit {
//#Input('report') public report: IReportInstance;
cOptions:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.cOptions = {nicolas: 'nicolas1'}; //change this in the next component that use the directive
}
}
If that is the case, your problem is that gServ is provide at the same rootComponent. with angular, service provider at the same rootComponent are singleton.
And use the same type in your directive and your component!!
I am having a hard time using a async object in a html composition.
Here is my model:
export class Version {
isGood: boolean;
constructor(isGood: boolean) {
this.isGood= isGood;
}
}
This model is called by a component as follows:
#Injectable()
export class MyComponent {
public version: Version;
constructor(private _myService: VersionService) {}
getVersion(): void {
// async service that gets the versions
this._myService.getVersion().subscribe(
data => this.version= data,
error=> console.log(error),
() => console.log("getting all items complete")
);
}
}
My template references to the version variable as follows:
<button (click)="getVersion()">Get Version</button>
<hr>
<p style="color:red">{{error}}</p>
<h1>Version</h1>
<p>{{version.isGood}}</p>
However, I get an exception:
Cannot read property 'isGood' of undefined
From scavenging the internet, I see that my problem is because the version object is null. If I do something like:
<p>{{version | json}}</p>
I can see the correct version
If I do something like
<p>{{version.isGood | async}}</p>
I see nothing
If I edit MyComponent, and set
public version: Version = new Version();
I can execute the .isGood property fetch, but it is always empty.
Is there a different way I am supposed to load a property if I am using it in an asynchronous manner?
Use the ? operator or use an *ngIf.
<p>{{version?.isGood}}</p>
<p *ngIf="version">{{version.isGood}}</p>
Try this:
<p>{{version?.isGood}}</p>
This tells Angular to protect against version.isGood being undefined or null until you click and fetch the data for version through your service.
First me correct you. #Injectable() makes a normal typescript class as injectable service where you can share data.
To make a component you need to use #Component decoratore.
The process of data sharing between component and within the application is to create a service and add that as provides in module. And then its singleton object will available everyshere.
//module
import {NgModule} from '#angular/core';
import {YourService} from "./services/your-service";
#NgModule({
imports: [
BrowserModule
],
declarations: [
AppComponent
],
providers: [
YouService
],
bootstrap: [AppComponent]
})
export class AppModule {
}
//this is your component
import {Component} from '#angular/core';
import {YourService} from "../../services/your-service";
#Component({
selector: 'component-app',
templateUrl: '../../views/app.component.html',
})
export class HeaderComponent {
constructor(public yourService: YourService) {
}
}
//your service
import {Injectable} from "#angular/core";
#Injectable()
export class YourService {
private _message: string = 'initial message';
private _style: string = 'success';
get message(): string {
return this._message;
}
set message(value: string) {
this._message += value;
}
get style(): string {
return this._style;
}
set style(value: string) {
this._style = value;
}
}
//finally your view
<div class="row">
<div [class]=""><h1>{{swapService.message}}</h1></div>
</div>
Observable Data services.
#Injectable()
export class MyComponent {
public version = new ReplaySubject<Version>();
constructor(private _myService: VersionService) {}
init(): void {
// async service that gets the versions
this._myService.getVersion().subscribe(
data => this.version.next(data),
error=> console.log(error),
() => console.log("getting all items complete")
);
}
getVersion(): void {
this.version.asObservable();
}
}
In the template
<button (click)="init()">Get Version</button>
<hr>
<p style="color:red">{{error}}</p>
<h1>Version</h1>
<p>{{(version |async)?.isGood}}</p>
I want to modify the field of a component instance.
For example, in test.component.ts:
#Component({
selector: 'test',
})
export class TestComponent {
#Input() temp;
temp2;
constructor(arg) {
this.temp = arg;
this.temp2 = arg * 2;
}
}
I want to set the values of temp and temp2 using the constructor. I know one approach is to use input property by doing something like:
<test [temp]='1'></test>
However, this is done after the constructing time and temp2 won't change accordingly. How can I supply component constructor argument from a consumer's point of view, such that the value of "temp" and "temp2" are set at constructing time?
Thanks!
In fact inputs of a component are only available from the ngOnInit method because of the component lifecycle:
#Component({
selector: 'test',
})
export class TestComponent {
#Input() temp;
ngOnInit() {
console.log(this.temp);
}
}
Moreover we can only use parameters in the component constructor that are provided through dependency injection.
So you can't use the constructor for the temp property because the component lifecycle. Regarding it depends on how you make it available. If it's through dependency injection, it will work but you need to use the #Inject decorator to specify what to inject.
You could also have a look at this question for more details:
Difference between Constructor and ngOnInit
sharedServcie.ts
import {Injectable} from 'angular2/core';
#Injectable()
export class sharedService{
test:string="Angular2";
}
boot.ts
import {sharedService} from './sharedService';
...
...
bootstrap[App,[sharedService]]
import {sharedService} from './sharedService';
#Component({
selector: 'test',
})
export class TestComponent {
temp;
constructor(sharedService:sharedService) {
this.temp = sharedService.test;
console.log(this.temp) //Angular2
}
}
I think the answer Thierry Templier explains your problem, but
you say in a comment:
I updated the question, hope this can be more clear. By using input
property, I can only change temp, but temp2 will not update
accordingly.
I hope this is what you want to achieve and help you.
import {Input, Component} from 'angular2/core'
#Component({
selector: 'my-test',
template: `
<h1> arg value: {{ arg }} </h1>
<h1> temp value: {{ temp }} </h1>
<h1> temp1 value: {{ temp1 }} </h1>
`
})
export class test {
#Input() arg : number;
temp : number;
temp1: number;
constructor(){
}
ngOnInit(){
this.temp = this.arg;
this.temp1 = this.arg * 2;
}
}
#Component({
selector: 'my-app',
directives: [test],
template: `
<h2>Hello {{name}}</h2>
<my-test [arg]="1"></my-test>
`
})
export class App {
constructor() {
this.name = 'Angular2';
}
}
test Plunker
Is there a way to write a global selfmade mylogger function that I could use in Angular2 typescript project for my services or components instead of console.log function ?
My desired result would be something like this:
mylogger.ts
function mylogger(msg){
console.log(msg);
};
user.service.ts
import 'commons/mylogger';
export class UserService{
loadUserData(){
mylogger('About to get something');
return 'something';
};
};
You could write this as a service and then use dependency injection to make the class available to your components.
import {Injectable, provide} from 'angular2/core';
// do whatever you want for logging here, add methods for log levels etc.
#Injectable()
export class MyLogger {
public log(logMsg:string) {
console.log(logMsg);
}
}
export var LOGGING_PROVIDERS:Provider[] = [
provide(MyLogger, {useClass: MyLogger}),
];
You'll want to place this in the top level injector of your application by adding it to the providers array of bootstrap.
import {LOGGING_PROVIDERS} from './mylogger';
bootstrap(App, [LOGGING_PROVIDERS])
.catch(err => console.error(err));
A super simple example here: http://plnkr.co/edit/7qnBU2HFAGgGxkULuZCz?p=preview
The example given by the accepted answer will print logs from the logger class, MyLogger, instead of from the class that is actually logging.
I have modified the provided example to get logs to be printed from the exact line that calls MyLogger.log(), for example:
get debug() {
return console.debug.bind(console);
}
get log() {
return console.log.bind(console);
}
I found how to do it here: https://github.com/angular/angular/issues/5458
Plunker: http://plnkr.co/edit/0ldN08?p=preview
As per the docs in developers.mozilla,
The bind() method creates a new function that, when called, has its
this keyword set to the provided value, with a given sequence of
arguments preceding any provided when the new function is called.
More information about bind here:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
If you want to use 'console.log' function just in your component you can do this:
import { Component, OnInit } from '#angular/core';
var output = console.log;
#Component({
selector: 'app-component',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
constructor() { }
ngOnInit() { }
printFunction(term: string): void {
output('foo');
}
}
How about using console on your main service, So we can customize and apply console.log conditionally:
myComponent.ts
export class myComponent implements OnInit {
constructor(
private config: GlobalService
) {}
ngOnInit() {
this.config.log('func name',{a:'aval'},'three');
}
}
global.service.ts
#Injectable()
export class GlobalService {
constructor() { }
this.prod = true;
public log(one: any, two?: any, three?: any, four?: any) {
if (!this.prod) {
console.log('%c'+one, 'background:red;color:#fff', two, three, four);
}
}
}
(Note: first parameter should be string in this example);
For toggling console.log ON\OFF:
logger.service.ts:
import { Injectable } from '#angular/core';
#Injectable()
export class LoggerService {
private oldConsoleLog = null;
enableLogger(){
if (this.oldConsoleLog == null) { return; }
window['console']['log'] = this.oldConsoleLog;
}
disableLogger() {
this.oldConsoleLog = console.log;
window['console']['log'] = function () { };
};
}
app.component.ts:
#Component({
selector: 'my-app',
template: `your templ;ate`
})
export class AppComponent {
constructor(private loggerService: LoggerService) {
var IS_PRODUCTION = true;
if ( IS_PRODUCTION ) {
console.log("LOGGER IS DISABBLED!!!");
loggerService.disableLogger();
}
}
}
I created a logger based on the provided information here
Its very basic (hacky :-) ) at the moment, but it keeps the line number
#Injectable()
export class LoggerProvider {
constructor() {
//inject what ever you want here
}
public getLogger(name: string) {
return {
get log() {
//Transform the arguments
//Color output as an example
let msg = '%c[' + name + ']';
for (let i = 0; i < arguments.length; i++) {
msg += arguments[i]
}
return console.log.bind(console, msg, 'color:blue');
}
}
}
}
Hope this helps
type safer(ish) version with angular 4, typescript 2.3
logger.service.ts
import { InjectionToken } from '#angular/core';
export type LoggerService = Pick<typeof console,
'debug' | 'error' | 'info' | 'log' | 'trace' | 'warn'>;
export const LOGGER_SERVICE = new InjectionToken('LOGGER_SERVICE');
export const ConsoleLoggerServiceProvider = { provide: LOGGER_SERVICE, useValue: console };
my.module.ts
// ...
#NgModule({
providers: [
ConsoleLoggerServiceProvider,
//...
],
// ...
my.service.ts
// ...
#Injectable()
export class MyService {
constructor(#Inject(LOGGER_SERVICE) log: LoggerService) {
//...
There is now an angular2 logger component on NPM which supports log levels.
https://www.npmjs.com/package/angular2-logger