Asked  7 Months ago    Answers:  5   Viewed   32 times

In Angular 1.x.x you simply ask for the same service and you end up with the same instance, making it possible to share the data in the service.

Now in Angular 2 I have a component that has a reference to my service. I can read and modify the data in the service, which is good. When I try to inject the same service in another component, it seems as if I get a new instance.

What am I doing wrong? Is it the pattern itself that is wrong (using a service to share data) or do I need to mark the service as a singleton (within one instance of the app) or something?

I'm on 2.0.0-alpha.27/ btw

I inject a service through appInjector (edit: now providers) in the @Component annotation and then save a reference in the constructor. It works locally in the component - just not across components (they do not share the same service instance) like I thought they would.

UPDATE: As of Angular 2.0.0 we now have @ngModule where you would define the service under the providers property on said @ngModule. That will ensure the same instance of that service to be passed to each component, service, etc. in that module. https://angular.io/docs/ts/latest/guide/ngmodule.html#providers

UPDATE: A lot has happened to Angular and FE development in general. As @noririco mentioned, you could also use a state management system like NgRx: https://ngrx.io/

 Answers

97

A service singleton is a nice solution. Other way - data/events bindings.

Here is an example of both:

class BazService{
  n: number = 0;
  inc(){
    this.n++;
  }
}

@Component({
  selector: 'foo'
})
@View({
  template: `<button (click)="foobaz.inc()">Foo {{ foobaz.n }}</button>`
})
class FooComponent{
  constructor(foobaz: BazService){
    this.foobaz = foobaz;
  }
}

@Component({
  selector: 'bar',
  properties: ['prop']
})
@View({
  template: `<button (click)="barbaz.inc()">Bar {{ barbaz.n }}, Foo {{ prop.foobaz.n }}</button>`
})
class BarComponent{
  constructor(barbaz: BazService){
    this.barbaz = barbaz;
  }
}

@Component({
    selector: 'app',
    viewInjector: [BazService]
})
@View({
  template: `
    <foo #f></foo>
    <bar [prop]="f"></bar>
  `,
  directives: [FooComponent, BarComponent]
})
class AppComponent{}

bootstrap(AppComponent);

Watch live

Tuesday, June 1, 2021
 
Yarin
answered 7 Months ago
35

Jason is completely right! It's caused by the way dependency injection works. It's based on hierarchical injectors.

There are several injectors within an Angular2 application:

  • The root one you configure when bootstrapping your application
  • An injector per component. If you use a component inside another one. The component injector is a child of the parent component one. The application component (the one you specify when boostrapping your application) has the root injector as parent one).

When Angular2 tries to inject something in the component constructor:

  • It looks into the injector associated with the component. If there is matching one, it will use it to get the corresponding instance. This instance is lazily created and is a singleton for this injector.
  • If there is no provider at this level, it will look at the parent injector (and so on).

So if you want to have a singleton for the whole application, you need to have the provider defined either at the level of the root injector or the application component injector.

But Angular2 will look at the injector tree from the bottom. This means that the provider at the lowest level will be used and the scope of the associated instance will be this level.

See this question for more details:

  • What's the best way to inject one service into another in angular 2 (Beta)?
Friday, June 4, 2021
 
SilverHorn
answered 7 Months ago
29

You can do a $rootScope.$broadcast on items that you need to sync across directive.

Or you can pass a object to your directive1 isolated scope, which would act as a communication mechanism. On this object if you change sub property like tablename, that would affect in the parent scope.

Something like

One23SRCApp.directive('directive1',function() {
    return {
        restrict: "A",
        scope:{tableconfig:'='},
        link: function (scope, element, attrs) {
           scope.tableconfig.tablename= "table";
        }
    };
});


One23SRCApp.directive('directive2',function() {
    return {
        restrict: "A",
           link: function (scope, element, attrs) {
           var tablename = scope.tableconfig.tablename;
        }
    };
})

The HTML becomes

<table directive1 tableconfig='tableconfig'>
  <tr>
     <td>column1</td>
   <td>column1</td>
   </tr>
</table>

Your controller should have this object defined

$scope.tableconfig={};

Saturday, July 17, 2021
 
brombeer
answered 5 Months ago
89

There is a very simple approach and a very complex one.

The simple approach is to use raw HTML with anchor element outside of angular without RouterLink. Register to clicks on that anchor element and use the Router service to navigate.

The task was to fire links but the actual problem is far deeper, now it links next time its showing an angular component...

So, for the complex solution:

This is an highly advanced topic... Not only it involves using advanced angular techniques it's also advanced in the leaflet implementation.

I'll do my best to convey the message but due to the complexity the examples will be very simple and will require work.

First - Angular realm.

An HTML string that contains directives, components or pipes will never work, the only way is to initialize a View

Let's define A View as a reference to view instance of a component or a template.

These are called ComponentRef and TemplateRef

So, we have 2 ways to solve this problem. Since I can't do both i'll go with ComponentRef but note that you can also use TemplateRef. With templates you'll first need to obtain a template defined in the component as well as a ViewContainerRef to attach that template to.

We will build a service that accepts a leaflet Marker and binds to the click event of the marker, on click it will open a popup which is an angular Component.

The component is simple, it renders a link.

@Component({
  selector: 'facility-link',
  template: `Facility <br/> <a routerLink="{{link}}"> View Two</a>`
})
export class FacilityLinkComponent {
  public link: string;
  constructor() { }
}

Now, for the service:

@Injectable()
export class LinkPopupService {

  constructor(private cfr: ComponentFactoryResolver,
              private injector: Injector,
              private appRef: ApplicationRef) { }


  register(marker: leaflet.Marker, link: string): void  {
    marker.on('click', ($event: leaflet.MouseEvent)  => this.popup($event.target, link) );
  }

  popup(marker: leaflet.Marker, link: string) {
    const cmpFactory = this.cfr.resolveComponentFactory(FacilityLinkComponent);
    const componentRef = cmpFactory.create(this.injector);
    componentRef.instance.link = link;
    this.appRef.attachView(componentRef.hostView);
    const markerElement = marker.getElement();
    markerElement.parentElement.appendChild(componentRef.location.nativeElement);

    const markerPos = leaflet.DomUtil.getPosition(markerElement);
    const markerClass = leaflet.DomUtil.getClass(markerElement);


    leaflet.DomUtil.setTransform(componentRef.location.nativeElement, markerPos);
    leaflet.DomUtil.setClass(componentRef.location.nativeElement, markerClass);
  }
}

The register method accepts a marker and the link and registers to the click event.

When the popup method fires it uses angular tools to create a view instance of FacilityLinkComponent, set the link for future binding, attach a view to it and attach it to the DOM.

This all happens in the first 5 lines of code.

Some notes:

  • We must attach a view so change detection works
  • A Proper implementation will allow to set ViewContainerRef and / or an Injector - this is a must when using lazy loading.
  • It is preferred sending data to the component via Injector and not by assignment (ReflectiveInjector)
  • Proper clean up is required (destroy the component and detach the view)
  • Need to add toggle logic, also clean on navigation.

Leaflet

The code from the 6th line performs positioning of the popup.

This is a very simple logic, it just copies everything from the marker.

This is why I used a marker, so I'll have a reference to take the positioning from.

In a realworld example you'll need to get a panel and push the components into their own layer, computing the position. This is not that difficult since leaflet has all the helper, but it was too much for this.

Hope it helps.

Wednesday, July 28, 2021
 
hakimoun
answered 5 Months ago
16

Try to import MdRippleModule in your AppModule:

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent }  from './app.component';
import { HttpModule} from '@angular/http';
import { MdRippleModule } from '@angular2-material/core/core'; <== this line

@NgModule({
  imports:      [ BrowserModule, HttpModule, MdRippleModule ], <== add here
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

Or pass MdRipple directive within your component:

...
import { MdRipple } from '@angular2-material/core/core'; <== this line
@Component({
  moduleId: module.id,
  selector: 'nav-bar',
  templateUrl: 'nav-bar.component.html',
  styleUrls: ['nav-bar.component.css'],
  directives: [ 
    MdToolbar, 
    MdButton,
    MdIcon,
    MdRipple <== add here
  ],
  providers: [ 
    MdIconRegistry 
  ]
})
export class NavBarComponent implements OnInit {
...
Tuesday, November 9, 2021
 
napolux
answered 4 Weeks ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :  
Share