-1

At the moment I am trying to learn Typescript and AngularJS coming from a background of Actionscript. I have been playing around with some code experiments and van generally achieve what I want to but not always in the way that I would like!

At the moment I have this code:

export class CountingService
{
    countScope = { count : 0 };

    public increment()
    {
        this.countScope.count++;
    }
}

export class PageController
{
    constructor( private service : CountingService )
    {
        this.countScope = service.countScope;
    }

    public localCount : number = 0;

    countScope;

    public increment()
    {
        this.service.increment();

        this.localCount++;
    }

}

and I have a few different views that have this code in:

<div ng-controller="PageController as page">

    <Button ng-click="page.increment()">Count {{page.localCount}}/{{page.countScope.count}}</Button>

</div>

When I switch between the views the localCount always resets to zero but the count in the countScope from the service does not reset and is incremented by each different controller.

This works but it's a bit messy. I have to have the untyped localScope floating around and the views need to know about the internal structure of the countScope object and bind to page.countScope.count rather than something like page.globalCount.

In Actionscript I would have a read only getting on the controller. The service would dispatch an event each time the value changed and the controller would listen for this and then update it's own property which would then update the view.

My question is what is the best practice way to achieve what I am doing here that does not require the view to have knowledge of the internals of an untyped object. I am pretty sure that public getters do not exist but can I dispatch and listen for an event?

Many Thanks

Roaders
  • 4,373
  • 8
  • 50
  • 71
  • Use static property, or save it in a service itself. Reason is that if you have instace properties to store localCount, everytime you switch the page and comeback the controller would have reinstantiated itself again. – PSL Dec 27 '14 at 19:56
  • How do you use statics in Angular / Typescript? I had a quick google and couldn't find anything. I want to save the value as a property of the service but if I do that how do I then display it on the html page. I only want my html to depend on the controller. – Roaders Dec 27 '14 at 20:00
  • Ignore if i misunderstood your question. You want to retain the count inside the controller right? And yes in your class you could define say `static localCount:number`; and set it to 0 in the constgructor if it is undefined. – PSL Dec 27 '14 at 20:01
  • Really I want to know the best way to update my html view of a value that is stored in a service that is injected into my controller. Thanks for the replies. – Roaders Dec 27 '14 at 20:02
  • I think i had misunderstood your question. What you reall need to do is not to do the business logic in the view, instead do it in the controller. i.e `` and in the controller on increment set `this.countRatio = this.localCount/this.countScope.count` – PSL Dec 27 '14 at 20:05
  • Don't put business logic in the controller. Leave the business logic in the service and push the relevant values to the controller via `$scope.$watch`. – John Kurlak Dec 27 '14 at 20:06
  • Do not create unwanted watches, The purpose of the controller really is to build the view model for your view and abstract out any kind of logic from the view. Most cases you get forced to use watches in the controller to accommodate design issues. – PSL Dec 27 '14 at 20:07
  • ignore the count ratio bit. I don't want to divide it. That's just how I was displaying it. – Roaders Dec 27 '14 at 20:07
  • In the MVC pattern, controllers query models to build the view model. In Angular, your service acts as your model and your scope acts as your view model. A watch is a mechanism for querying your service. – John Kurlak Dec 27 '14 at 20:09
  • I want to watch the value, I want to controller (in my mind the class responsible for presentation logic) to be informed when the value in the service (the business logic) updates so that the controller can update a value that is bound to by the view. @JohnKurlak - can I $watch a value on the service? Seems like just what I want. – Roaders Dec 27 '14 at 20:09
  • @Roaders Yes. You can do something like this in your controller's constructor (or in a method called by the constructor): `$scope.$watch(() => service.countScope, (serviceCountScope) => this.countScope = service.countScope);` (Warning, untested). Don't forget to inject $scope into your controller. – John Kurlak Dec 27 '14 at 20:10
  • @JohnKurlak angular is not necessarily an `MVC` it could be considered as `MV*` based on how you design your system. And watch is a querying mechanism.. Wow thats new. :) Do you realize the watches run during every digest cycle? And yes you could abstact out business logic to service and just call the service method from the controller... Its not going to be as bad a design as using a watch – PSL Dec 27 '14 at 20:11
  • @PSL I agree, but as soon as you move business logic to your controller, you reduce the reusability of your code. Yes, I realize that watches execute during every digest cycle. In this case, this is a low cost watch. If you move the logic to the service, when do you call the method in the service? You need to know when the value updates. – John Kurlak Dec 27 '14 at 20:12
  • @JohnKurlak That is a low cost watch.. :) do you think OP is doing that simple thing in the use case and there could just be say 10 additional such low cost watches.. :) `You need to know what the value updates` -> This is where pub/sub mechanism comes into picture. – PSL Dec 27 '14 at 20:15
  • @PSL The difference between 1 low-cost watch and 10 low-cost watches is very small. You would need 100-1000 low-cost watches before you noticed a performance issue. – John Kurlak Dec 27 '14 at 20:17
  • @JohnKurlak You will think that and keep adding on such watches and end you will realize that you really are running unwanted watch evaluator functions when you really do not need to. I mean atleast i would rethink before using a watch. But i must say i really do not have an answer for the little context in the question. So i stop here. :) – PSL Dec 27 '14 at 20:17
  • @PSL Pub/sub is a viable option, but it's going to be more complicated to implement in native Angular. – John Kurlak Dec 27 '14 at 20:19
  • @JohnKurlak It is never complicated to implement a pub sub. A simple built in pusub implementation is nothing but angular event bus. – PSL Dec 27 '14 at 20:19
  • @PSL Then add it as an answer :) – John Kurlak Dec 27 '14 at 20:19
  • 1
    @JohnKurlak As i said i have very little context on this question and i dont have an answer(of course that is why i did not answer :)). But i have a simple pubsub design which i [answered here](http://stackoverflow.com/questions/25274563/angularjs-communication-between-directives) But it all wires up with a better component/feature based design. But you could probably add watch answer of yours as you think just some small inexpensive watches and probably it also suits OP current requirement. – PSL Dec 27 '14 at 20:21
  • @PSL You and I have a different understanding of the word "complicated" (I see that code and think about memory-leak testing, unit testing, etc. that all have to be done to make it production ready) (and maybe changes to allow multiple subscriptions on a single scope). That being said, your pub/sub approach is valid and should be posted as an answer. – John Kurlak Dec 27 '14 at 20:26
  • 1
    @PSL I don't like the idea of having to take a dependency on a $scope in a service. With the pub/sub model, the service would have to have a scope that it publishes to when changes occur. – John Kurlak Dec 27 '14 at 20:29
  • @JohnKurlak Not necessarily you could just use it as a service and remove dep on the scope. But it is just for angular design and ofcourse another alternative to existing angular event bus (which is on scope as well). That is one good thing about putting a utility method on the rootScope. But you are right and that said you have a valid point there, it could well be its own service with just an optional scope wrapper. – PSL Dec 27 '14 at 20:32
  • There is an answer here: http://stackoverflow.com/a/17558885/151770 that would work but that's quite a bit of code... – Roaders Dec 27 '14 at 20:34
  • @PSL, but if you moved it to a service, you'd still have to unregister your subscriptions when $scope.$destroy is called. That would either mean having references to a $scope in your service (a bad idea from the perspective of memory leak management) or you would have to manually do the unsubscribing in each scope that subscribes (a step that can easily be forgotten and result in memory leaks). It's not impossible to manage unsubscribing automatically, but it makes this solution more difficult to get right. – John Kurlak Dec 27 '14 at 20:35
  • @Roaders The answer you linked to is essentially the same as what PSL is suggesting :) – John Kurlak Dec 27 '14 at 20:36
  • @Roaders See Jamie's answer on the page you linked to. That essentially covers the concerns I've listed in my comments above, but as Jamie points out, "Some may object to passing the $scope to a service" – John Kurlak Dec 27 '14 at 20:39
  • @JohnKurlak Ofcourse that is why you have scope.destroy event and _the_ purpose of an angular based implementation. And in other case unsubscribing the event wires down to as a primary implementation requirement in any pub/sub design patterns. – PSL Dec 27 '14 at 20:40
  • 2
    Thanks guys. I've learnt a lot from your discussion. I will update this question with at least one answer when I've played with the various different approaches. – Roaders Dec 27 '14 at 20:41
  • @PSL Ultimately, the pub/sub approach requires having a dependency on some scope in a service. Some consider this a bad practice because it does not maintain a separation of concerns. That being said, I think Roaders can decide what option is best for his particular scenario. Good discussion! – John Kurlak Dec 27 '14 at 20:43
  • @JohnKurlak I do not think you would ever need to inject scope in a service (That is a code smell right there). But again as i said multiple approaches you can take placing the methods on rootScope just like angular does (my example) or use it as a service as is. I dont know how there is an SOC issue unless you mistook on how it is used (and there is no question of testability), and how there could be a leak issue if you expose methods on the scope and destroy handler when scope is destroyed( I have never seen it alteast in my profiling). And yeah, Nice discussion! – PSL Dec 28 '14 at 00:55
  • @PSL At some point in time the service needs to notify subscribers that a value has changed. It would presumably do this via `$rootScope.publish()`, but in order to do that, you need to inject `$rootScope` into your service. The other option you mention is to create a service for doing pub/sub. In that case, you would do `myPubSubService.publish()` from your service, which is fine, but the declaration for `MyPubSubService` would have to deal with scopes (in order to auto unsubscribe when a scope is destroyed). I don't see any safe way to avoid having a reference to a scope in some service. – John Kurlak Dec 28 '14 at 05:26
  • @JohnKurlak That has been my exact point from the beginning, That is not the point.. Seeing just a small part of the problem( in the question) which is just an X/Y problem part of a possible design constraint. You don't have services communicate (atleast in my experience) to another component. Instead components communicate by pub/sub, event bus etc via their controllers, directives etc.. Services do not control the behavior they play supporting role building up the model, ajax call, utilities, and so on for the component. – PSL Dec 28 '14 at 05:45
  • @JohnKurlak If you do raise the event from the service then service is doing too much apart from just managing the data it needs to, instead it has to come from somebody (a controller or some body who originates the original action) who does something and calls increment function on the service. increment the variable on the service is not really the original action, it is a side effect which makes somebody to call the service to perform the job. – PSL Dec 28 '14 at 05:50
  • @JohnKurlak And if at all it is really originating from a service (Something like a polling) then there is a promise pattern with notify action you can easily use, which keeps on notifying subscribers about something that has happened internal. And like i said, i really do not want to settle with a solution with just a piece of puzzle.. – PSL Dec 28 '14 at 05:53
  • @PSL OK, now I think I understand what you're saying, and I agree with it. Thanks for clarifying! One thing to note is that this approach is hard to work with when you have lots of directives and controllers all using a single service. Any time one of those directives or controllers needs to update a value in the service, it must publish an event. This is bookkeeping that can easily be forgotten and result in bugs. (AngularJS itself uses the same approach with its $scope/$digest model. It's easy to forget to call $scope.$digest() when you update your model.) – John Kurlak Dec 28 '14 at 16:54
  • @PSL In summary, I'd say both your approach and my approach are clean from an abstraction point of view. In my opinion, your approach is faster in terms of performance, whereas my approach is easier to maintain. – John Kurlak Dec 28 '14 at 16:55

5 Answers5

1

Finally answer three. I didn't expect this to work but it does and IMHO this is by far the nicest solution.

It enforces encapsulation with getters, it doesn't need $scope injected and there are no concerns about memory leaks.

I welcome any criticisms that point out issues that I am not aware of.

The only thing that I AM aware of is that you need to enable ECMAScript 5 which means that IE 7 and 8 are not supported. That's fine for me.

export class CountingService
{
    private _serviceCount : number = 100;

    public increment()
    {
        this._serviceCount++;
    }

    get serviceCount() : number
    {
        return this._serviceCount;
    }
}

export class PageController
{
    constructor( private countingService : CountingService )
    {}

    private _localCount : number = 0;

    get localCount() : number
    {
        return this._localCount;
    }

    get serviceCount() : number
    {
        return this.countingService.serviceCount;
    }

    public increment()
    {
        this.countingService.increment();

        this._localCount++;
    }
}
Roaders
  • 4,373
  • 8
  • 50
  • 71
  • You can use TypeScript to make public methods that behave like getters. – John Kurlak Dec 28 '14 at 16:34
  • I think this is closer to what PSL was suggesting (but it took a number of comments back and forth for me to determine that). This approach is fine if it works for your situation. The disadvantage is that if you ever have multiple controllers that need to increment the `countingService`, you'll have to have code in each of them to do the incrementing. – John Kurlak Dec 28 '14 at 16:47
  • I have a test where multiple controllers all call the increment count on the service and all the displayed values update fine... – Roaders Dec 30 '14 at 10:32
1

This seems like the perfect fit for a reactive library and is exactly what we do. I highly recommend you check out RxJs which has some awesome features. https://github.com/Reactive-Extensions/RxJS/tree/master/doc

https://www.youtube.com/watch?v=XRYN2xt11Ek

You code using Rx

export class CountingService {

    //Create a stream of changes.  When caller subscribe always give them the most recent value. 
    private rawCountStream = new Rx.Subject<number>();
    public countStream : Rx.Observable<number>;
    private count = 0;

    constructor() {
        this.countStream = this.rawCountStream.replay(1);
    }

    public increment() {
        this.count++;
        //Pump the value to callers. 
        this.rawCountStream.onNext(this.count);
    }
}

export class PageController {

    constructor(private service: CountingService) {
        //Listen to the stream of changes. 
        service.countStream.subscribe(value => {
            //do something
            //IMPORTANT If you want angular to update the ui you will need to call $apply on a scope. 
            //RXJS has a scheduler for this so you look at that here. 
            //https://github.com/Reactive-Extensions/rx.angular.js
            }
        );
    }

    public localCount: number = 0;

    public increment() {
        this.service.increment();
        this.localCount++;
    }
}
CompareTheMooCat
  • 1,047
  • 2
  • 10
  • 14
  • That is actually much closer to how I would like to implement it and closest to how I would do it in actionscript (the language I am used to). I will leave the $watch answer as the accepted answer as I think most people will find that useful and as it does not require an extra library that people might not want to use. I think that this is the best implementation though and I will use this. – Roaders Dec 30 '14 at 11:45
0

This is answer number 1 and I think is the approach that John Kurlak was talking about:

export class CountingService
{
    serviceCount : number = 100;

    public increment()
    {
        this.serviceCount++;
    }
}

export class PageController
{
    constructor( $scope, private service : CountingService )
    {
        this.serviceCount = service.serviceCount;

        $scope.$watch( () => this.service.serviceCount, ( newValue : number, oldValue : number ) => this.updateLocal( newValue, oldValue ) );
    }

    localCount : number = 0;

    serviceCount : number;

    public increment()
    {
        this.service.increment();

        this.localCount++;
    }

    private updateLocal( newValue : number, oldValue : number )
    {
        this.serviceCount = newValue;
    }

}

This works and I think is the sort of solution that I was after. There are things that I don't like about it though.

I don't like having to inject the $scope into my service. It seems to be an extra dependency on something that I shouldn't need. I also really don't like the public members that can be updated by anything and break encapsulation. I want to fix this with getters but I can't figure out how to update to ECMAScript 5 (question here: Targeting ES5 with TypeScript in IntelliJ IDEA 14).

John - is this what you were proposing? If anyone comments with Pros and Cons to this answer I'd be very grateful. I will mark this as the answer as I think this is what I was after to start with.

Community
  • 1
  • 1
Roaders
  • 4,373
  • 8
  • 50
  • 71
  • This is essentially what I had in mind. You could probably simplify it a bit by putting an `update()` function on the service and calling it directly inside of your watch instead of calling `updateLocal()`. That would be more "OOP-friendly" than the current approach, which technically breaks encapsulation. One disadvantage of this approach is that you now have a watch which must evaluate every time your digest loop executes. – John Kurlak Dec 28 '14 at 16:32
  • Thanks for the comment but I don't understand your bit about update() vs updateLocal(). What would the update function on the service do? In my implementation the service is responsible for updating it's count and the controller is responsible for updating it's copy of the service count (this copy of the data is another aspect I don't like). Surely you're not proposing that the service updates the count value in the controller? – Roaders Dec 30 '14 at 10:31
0

This is answer number 2 and I think this is what PSL was getting at (or something similar)

interface CountingCallBack
{
    parent : any;
    callback : ( value : number ) => void;
}

export class CountingService
{
    private _observerCallBacks : Array<CountingCallBack> = [];

    private _serviceCount : number = 100;

    public increment()
    {
        this._serviceCount++;

        this.notifyObservers();
    }

    public registerObserverCallback( callbackParent : any, callbackFunction : ( value : number ) => void )
    {
        var callback : CountingCallBack = { parent : callbackParent, callback : callbackFunction };

        this._observerCallBacks.push( callback );
        this.updateObserver( callback );
    }

    private notifyObservers()
    {
        angular.forEach( this._observerCallBacks, this.updateObserver, this );
    }

    private updateObserver( callback : CountingCallBack )
    {
        callback.callback.apply( callback.parent, [ this._serviceCount ] );
    }

}

export class PageController
{
    constructor( private countingService : CountingService )
    {
        countingService.registerObserverCallback( this, this.updateLocal );
    }

    localCount : number = 0;

    serviceCount : number;

    public increment()
    {
        this.countingService.increment();

        this.localCount++;
    }

    private updateLocal( newValue : number )
    {
        this.serviceCount = newValue;
    }
}

I'm quite pleased that I managed to implement this as I now understand how the function interface syntax works (and I quite like it) and I managed to fix the issue with the 'this' scope issue in the updateLocal function on the controller.

This solution doesn't require and $scope to be injected which is nice but I find the callback implementation quite messy. I don't like having to pass the controller and the function on the controller to the service to add a callback. I suppose I could create an interface, ICountListener or something, and just pass that to the service and then call updateCount on the controller. That would be a bit neater but still not that good. Is there a neater way around setting up the callback than this?

The biggest problem with this code (and a reason that it should not be used) is that it creates a memory leak. If you keep switching views the controllers never get cleaned up so you have many controllers left in memory all responding to the updated count. It would be relatively easy to clean up the callback and de-register a callback but that would presumably involve injecting a $scope and then listening for a destroy event (I don't know how to do this but I think this was under discussion in the comments above).

Roaders
  • 4,373
  • 8
  • 50
  • 71
-1

Short answer: Services are singletons i.e. the constructor is only called once. If you want it reset you can do that from the constructor of your controller.

this.service.countScope.count = 0

Notes

I see a lot of things I don't like about that code sample. But I am sure its just a sample so don't take it as a personal offence ;)

  • don't have a service called service. Call it countService.
  • don't have a member countScope on the service. Just use count. And don't copy countScope to the current controller. Just use this.countService.count

Update

The root of this question is how do I update a displayed value in html when a value in a service has changed

Since you have the controller as page and page.service is the service simply {{page.service.countScope.count}} would display teh count.

Community
  • 1
  • 1
basarat
  • 261,912
  • 58
  • 460
  • 511
  • I don't think the original poster wants to reset the value on the service. He wants to display the value in the service on the view. – John Kurlak Dec 28 '14 at 05:29
  • Thanks for the reply but this is not what I was after. I am not wanting to reset the count. The root of this question is how do I update a displayed value in html when a value in a service has changed. I totally agree about naming, this is not production code, this is me playing with a new language to see how it works. If I don't have the countscope object (which I really don't like) how do I bind to the value of the service count in the view? That's the whole point of the question. Thanks – Roaders Dec 28 '14 at 09:31