Angular2 Physics animation - javascript

I'm looking to perform some physics animations and have the animation carried out on a set of DOM elements. Not canvas. Very important: Not canvas.
I have it working, but the performance is slower than I anticipated even considering how expensive DOM manipulations are. It borders on unusable if you have more than a few components on the page at a time even if you adjust the interval to be less frequent.
I'm wondering if there is a simpler or more performant way while keeping things within Angular. Maybe a way to skip the Angular rendering of the zone altogether? Doing it vanilla without utilizing Angular bindings and such is way more performant so I'm wondering if I'm just doing the Angular portion wrong, or if I should break these sections free of Angular. I thought Zones were supposed to outperform global manipulation though...?
Example code to wiggle something across the screen (the real animations are more complicated but follow this exact technique):
#Component({
selector: 'thingy,[thingy]',
template: `<div #container [ngStyle]="getStyle()"><ng-content></ng-content></div>`
})
export class Thingy implements OnInit, OnDestroy {
private _x:number = 0;
private _y:number = 0;
private _interval:any;
private _style:CSSStyleDeclaration = {
left: 0,
top: 0,
position: absolute
};
constructor(){}
ngOnInit() {
this._interval = setInterval(() => {
this._x++;
this._y = Math.sin(this._x);
this._style.left = this._x + "px";
this._style.top = this._y + "px";
});
}
ngOnDestroy() {
clearInterval(this._interval); // because it continues ticking after destroy
}
getStyle():CSSStyleDeclaration {
return this._style; // in angular1 it was bad joojoo to return a new object each time so i avoid doing so here too
}
}
I've optimized this approach as much as I know how. I think the built-in animation metadata solution could handle most of the scenarios but I haven't tried because a) I can't imagine how adding more abstraction increases performance and b) these animations are not state transitions so it doesn't seem appropriate.
I've also tried using a template more like this but it doesn't seem to be much different:
<div [style.top.px]="_y" [style.left.px]="_x"><ng-content></ng-content></div>
Also, I've tried directly messing with the ElementRef but that certainly didn't help:
#ViewChild("container") container:ElementRef;
this._container.nativeElement.styles.top = this._y + "px";
If it is best to do this outside of Angular's control, is there any standard for that? I can draw the DOM element with a Component and then dispatch a Window event to kickstart non-angular code...
Also of note: I cannot start at point A and jump immediately to point B in order to let CSS transitions paint the animation. The animations are not predictable enough to transition/ease. Unless theres a very clever solution, I don't see how it can be animated except for ticking through each step.

I solved the performance issue by using a global ticker instead of having each component responsible for its own. The ticker is a simple Observable which each component subscribes to and unsubscribes from where it would otherwise have started and stopped its own interval.
import {Injectable} from '#angular/core';
import {Observer, Observable} from 'rxjs';
#Injectable()
export class TickerService {
private _observer: Observer<number>;
private _timer:any;
public data$ = new Observable(observer => this._observer = observer).share();
constructor() {
this.data$ = new Observable(observer => {
this._observer = observer;
}).share();
}
public start():void {
if(this._timer) { // not required... just didn't want to have two going at once
this.stop();
}
this._timer = setInterval(() => {
this.tick();
}, 30);
}
public stop():void {
clearInterval(this._timer);
this._timer = 0;
}
private tick():void {
this._observer.next(new Date().getTime()); // the date part is irrelevant. i just wanted to use it to track the performance lag between each tick
}
}
Items can them react to it as such:
#Component({
selector: 'thingy,[thingy]',
template: `<div #container [ngStyle]="getStyle()"><ng-content></ng-content></div>`
})
export class Thingy implements OnInit, OnDestroy {
private _x:number = 0;
private _y:number = 0;
private _subscription:Subscription;
private _style:CSSStyleDeclaration = {
left: 0,
top: 0,
position: absolute
};
constructor(private _ticker:TickerService){}
ngOnInit() {
this._subscription = this._ticker.data$.subscribe(() => {
this._x++;
this._y = Math.sin(this._x);
this._style.left = this._x + "px";
this._style.top = this._y + "px";
});
this._ticker.start(); // even though every instance will call this, there's a guard in the TickerService to only allow one ticker to run
}
ngOnDestroy() {
this._subscription.unsubscribe();
}
getStyle():CSSStyleDeclaration {
return this._style;
}
}
This small change took the performance for 5 Thingys from about 5fps to about 60fps. I believe this to be because each Thingy spawned its own ticker, causing its own (blocking) digest/paint to run out of sequence with the others. This caused Angular to digest each change individually, and a giant stack built up. Instead of one tick with 5 updates to perform, it was performing 5 ticks with 1 update each.
So now, all changes are made and then executed within one digest/zone update.

Related

How to share data between sibling components in Angular

I am still learning and I got stuck so I need to ask a question. My understanding of Input Output decorators is that I need to add selector to html of parent to be able to use them, but for my case I don't think it's the way to go, but someone can prove me wrong.
CASE: For readability purposes I have split components. I have one component, data-fetch-transform that gets the data form local JSON file and does some adjustments to it, and another one, that wants to take that data for further use.
PROBLEM: I am unsure how to read the data from one component in the other. On the example below, how can I get countryNumber and centerNumber result in my other component. I intend to have data-fetch-transform.component.ts just manipulate the data and used in other components
Target component
project/src/app/data-use/data-use.component.ts
Data Source component
project/src/app/data-fetch-transform/data-fetch-transform.component.ts
import { Component, OnInit } from '#angular/core';
import * as data from '../../../../../data/Data.json';
#Component({
selector: 'app-datafetch-transform',
templateUrl: './datafetch-transform.component.html',
styleUrls: ['./datafetch-transform.component.css'],
})
export class DatafetchComponent implements OnInit {
public dataList: any = (data as any).default;
dataPointCount = this.data.length!!;
uniqueValues = (dt: [], sv: string) => {
var valueList: [] = [];
for (let p = 0; p < this.dataPointCount; p++) {
valueList.push(dt[p][sv]);
}
var uniqueValues = new Set(valueList);
return uniqueValues.size;
};
countryNumber=this.uniqueValues(this.dataList, 'Country')
centerNumber=this.uniqueValues(this.dataList, 'Center Name')
constructor() {}
ngOnInit(): void {}
}
You don't need another component for data manipulation (data-fetch-transform), you need a service (data-fetch-transform-service) where you should do the logic.
HERE IS WHAT YOU SHOULD HAVE IN THE SERVICE
private _dataList = new behaviorSubject([]);
public dataList$ = _dataList.asObservable();
for (let p = 0; p < this.dataPointCount; p++) {
// ... do your thing
_valueList.next(result);
}
and in the component you just subscribe to the service:
declarations:
private _subscription = new Subscription()
in constructor:
private dataService:DataFetchTransformService
and in ngOnInit:
this_subscription.add(this.dataService.dataList$.subscribe((response:any)=>{
this.data = response;
}))
in ngOnDestroy():
ngOnDestroy(){
this._subscription.unsubscribe();
}
I strongly suggest to stop using any since it can bring a lot of bugs up.
Also, as a good pattern, I always suggest use behaviorSubject only in the service as a private variable and user a public observable for data.
WHY IS BETTER TO USE A SERVICE
You can subscribe from 100 components and writing only 4 lines of code you bring the data anywhere.
DON'T FORGET TO UNSUBSRIBE IN ngOnDestroy
If you don't unsubscribe, you'll get unexpected behavior.

Angular 4 - Scroll Animation

I'm creating a webpage having full page width/height div's.
While scrolling down I've two types of methods.
Scroll on Click
//HTML
<a (click)="goToDiv('about')"></a>
//JS
goToDiv(id) {
let element = document.querySelector("#"+id);
element.scrollIntoView(element);
}
Scroll on HostListener
#HostListener("window:scroll", ['$event'])
onWindowScroll($event: any): void {
this.topOffSet = window.pageYOffset;
//window.scrollTo(0, this.topOffSet+662);
}
1. How to add a scrolling animation effects?
Just like :
$('.scroll').on('click', function(e) {
$('html, body').animate({
scrollTop: $(window).height()
}, 1200);
});
2. And how to use HostListener to scroll to next div?
You can also use the CSS property
scroll-behavior: smooth
in combination with
var yPosition = 1000;
window.scrollTo(0,yPosition)
Ref: developer.mozilla.org/docs/Web/CSS/scroll-behavior
This one is fun. The solution, as with most things angular 2, is observables.
getTargetElementRef(currentYPos: int): ElementRef {
// you need to figure out how this works
// I can't comment much on it without knowing more about the page
// but you inject the host ElementRef in the component / directive constructor and use normal vanillaJS functions to find other elements
}
//capture the scroll event and pass to a function that triggers your own event for clarity and so you can manually trigger
scrollToSource: Subject<int> = new Subject<int>();
#HostListener("window:scroll", ['$event'])
onWindowScroll($event: any): void {
var target = getTargetElementRef(window.pageYOffset);
this.scrollTo(target);
}
scrollTo(target: ElementRef): void {
// this assumes you're passing in an ElementRef, it may or may not be appropriate, you can pass them to functions in templates with template variable syntax such as: <div #targetDiv>Scroll Target</div> <button (click)="scrollTo(targetDiv)">Click To Scroll</button>
this.scrollToSource.next(target.nativeElement.offsetTop);
}
//switch map takes the last value emitted by an observable sequence, in this case, the user's latest scroll position, and transforms it into a new observable stream
this.scrollToSource.switchMap(targetYPos => {
return Observable.interval(100) //interval just creates an observable stream corresponding to time, this emits every 1/10th of a second. This can be fixed or make it dynamic depending on the distance to scroll
.scan((acc, curr) => acc + 5, window.pageYOffset) // scan takes all values from an emitted observable stream and accumulates them, here you're taking the current position, adding a scroll step (fixed at 5, though this could also be dynamic), and then so on, its like a for loop with +=, but you emit every value to the next operator which scrolls, the second argument is the start position
.do(position => window.scrollTo(0, position)) /// here is where you scroll with the results from scan
.takeWhile(val => val < targetYPos); // stop when you get to the target
}).subscribe(); //don't forget!
With a click this is easy to use. You just bind scrollTo to a click
This only works for scrolling in one direction, However this should get you started. You can make scan smarter so it subtracts if you need to go up, and instead use a function inside takeWhile that figures out the correct termination condition based on if going up or down.
edit: rxjs 5+ compatible version
this.scrollToSource.pipe(switchMap(targetYPos =>
interval(100).pipe( //interval just creates an observable stream corresponding to time, this emits every 1/10th of a second. This can be fixed or make it dynamic depending on the distance to scroll
scan((acc, curr) => acc + 5, window.pageYOffset), // scan takes all values from an emitted observable stream and accumulates them, here you're taking the current position, adding a scroll step (fixed at 5, though this could also be dynamic), and then so on, its like a for loop with +=, but you emit every value to the next operator which scrolls, the second argument is the start position
takeWhile(val => val < targetYPos)) // stop when you get to the target
)).subscribe(position => window.scrollTo(0, position)); // here is where you scroll with the results from scan
I spent days trying to figure this out. Being a newbie I tried many things and none of them work. Finally, I have a solution so I will post it here.
There are 2 steps:
Animate when things appear.
Make things appear when scrolling.
Part 1: I found out these two great tutorials for newbies:
The most basic one
The one that actually animates when stuff appears
Part 2: I simply find the solution in this answer
Part 1 Step by Step:
Add the line import { BrowserAnimationsModule } from '#angular/platform-browser/animations'; to /src/app/app.module.ts and then also:
#NgModule({
// Other arrays removed
imports: [
// Other imports
BrowserAnimationsModule
],
})
In the component.ts you want to animate, add: import { trigger,state,style,transition,animate } from '#angular/animations'; And then:
#Component({
// Here goes the selector and templates and etc.
animations: [
trigger('fadeInOut', [
state('void', style({
opacity: 0
})),
transition('void <=> *', animate(1000)),
]),
]
})
Finally, in the HTML item you want to animate, add [#fadeInOut].
If everything was done correctly, you should now have an animation (but it happens as soon as the webpage loads and not when you scroll.
Part 2 Step by Step:
Create a file .ts like for example appear.ts and copy-paste this code:
import {
ElementRef, Output, Directive, AfterViewInit, OnDestroy, EventEmitter
} from '#angular/core';
import { Observable, Subscription, fromEvent } from 'rxjs';
import { startWith } from 'rxjs/operators';
//import 'rxjs/add/observable/fromEvent';
//import 'rxjs/add/operator/startWith';
#Directive({
selector: '[appear]'
})
export class AppearDirective implements AfterViewInit, OnDestroy {
#Output()
appear: EventEmitter<void>;
elementPos: number;
elementHeight: number;
scrollPos: number;
windowHeight: number;
subscriptionScroll: Subscription;
subscriptionResize: Subscription;
constructor(private element: ElementRef){
this.appear = new EventEmitter<void>();
}
saveDimensions() {
this.elementPos = this.getOffsetTop(this.element.nativeElement);
this.elementHeight = this.element.nativeElement.offsetHeight;
this.windowHeight = window.innerHeight;
}
saveScrollPos() {
this.scrollPos = window.scrollY;
}
getOffsetTop(element: any){
let offsetTop = element.offsetTop || 0;
if(element.offsetParent){
offsetTop += this.getOffsetTop(element.offsetParent);
}
return offsetTop;
}
checkVisibility(){
if(this.isVisible()){
// double check dimensions (due to async loaded contents, e.g. images)
this.saveDimensions();
if(this.isVisible()){
this.unsubscribe();
this.appear.emit();
}
}
}
isVisible(){
return this.scrollPos >= this.elementPos || (this.scrollPos + this.windowHeight) >= (this.elementPos + this.elementHeight);
}
subscribe(){
this.subscriptionScroll = fromEvent(window, 'scroll').pipe(startWith(null))
.subscribe(() => {
this.saveScrollPos();
this.checkVisibility();
});
this.subscriptionResize = fromEvent(window, 'resize').pipe(startWith(null))
.subscribe(() => {
this.saveDimensions();
this.checkVisibility();
});
}
unsubscribe(){
if(this.subscriptionScroll){
this.subscriptionScroll.unsubscribe();
}
if(this.subscriptionResize){
this.subscriptionResize.unsubscribe();
}
}
ngAfterViewInit(){
this.subscribe();
}
ngOnDestroy(){
this.unsubscribe();
}
}
Import it using import {AppearDirective} from './timeline/appear';and add it to the imports as:
#NgModule({
declarations: [
// Other declarations
AppearDirective
],
// Imports and stuff
Somewhere in the class do:
hasAppeared : boolean = false;
onAppear(){
this.hasAppeared = true;
console.log("I have appeared!"); // This is a good idea for debugging
}
Finally, in the HTML add the two following:
(appear)="onAppear()" *ngIf="hasAppeared"
You can check this is working by checking the console for the message "I have appeared!".
The #bryan60 answer works, but I was not comfortable with it, and I preferred to use TimerObservable which seems less confusing for other teammates and also easier to customize for future uses.
I suggest you have a shared service for times you're touching DOM, or working with scroll and other HTML element related issues; Then you can have this method on that service (otherwise having it on a component does not make any problem)
// Choose the target element (see the HTML code bellow):
#ViewChild('myElement') myElement: ElementRef;
this.scrollAnimateAvailable:boolean;
animateScrollTo(target: ElementRef) {
if (this.helperService.isBrowser()) {
this.scrollAnimateAvailable = true;
TimerObservable
.create(0, 20).pipe(
takeWhile(() => this.scrollAnimateAvailable)).subscribe((e) => {
if (window.pageYOffset >= target.nativeElement.offsetTop) {
window.scrollTo(0, window.pageYOffset - e);
} else if (window.pageYOffset <= target.nativeElement.offsetTop) {
window.scrollTo(0, window.pageYOffset + e);
}
if (window.pageYOffset + 30 > target.nativeElement.offsetTop && window.pageYOffset - 30 < target.nativeElement.offsetTop) {
this.scrollAnimateAvailable = false;
}
});
}
}
scrollToMyElement(){
this.animateScrollTo(this.myElement)
}
You need to pass the element to this method, here is how you can do it:
<a (click)="scrollToMyElement()"></a>
<!-- Lots of things here... -->
<div #myElement></div>

Angular2: Directive variable change, update element

I'm currently trying to give classes to an wrapper that contains all of my app, i usually find this handy for giving certain states like when the header is fixed, or the menu's are opened etc.
So upon reading through some the docs of angular i should probably use an 'Directive'. Now i got this all set up and it looks like this:
constructor(private router:Router, #Inject(DOCUMENT) private document:Document, el:ElementRef, renderer:Renderer) {
this.setClasses(el, renderer);
}
setClasses(el:ElementRef, renderer:Renderer) {
renderer.setElementClass(el.nativeElement, 'header-fixed', this.headerFixed);
}
#HostListener("window:scroll", [])onWindowScroll() {
let number = this.document.body.scrollTop;
if (number > 100) {
this.headerFixed = true;
} else if (this.headerFixed && number < 10) {
this.headerFixed = false;
}
}
Now this is working perfectly but as you can see i'm toggling the headerFixed variable depending on scroll position. However i could of course run the function setClasses() again and it will work but is there anyway to subscribe/watch the variable and update automatically when changed?
Or is there even a better way of achieving wat i'm trying to do?
You can use #HostBinding like:
#HostBinding('class.header-fixed') get myClass() {
return someCondition;
}
Plunker Example

How to correctly wait for the elements to get their correct width before accessing it in Angular 2?

I'm trying to make buttons the same width but I'm having problems actually waiting for Angular 2 to finish all the rendering and stuff before accessing the width of the buttons.
I tried using DoCheck which "works", by "works" I mean that it does pick up the correct width value, but only the second time it runs.
Since NgDoCheck runs twice for some reason, as soon as I add the button.style.width = widest + 'px'; statement, the buttons will all get a width of 115px, so the second time ngDoCheck runs, it will go over all the buttons again, but this time all the buttons will be set to 115px, so widest can never become the correct value of 159px since we set the value the first time ngDoCheck ran.
What would be the correct way to go about this issue? Using DoCheck just seems wrong since it runs twice, and I'm not really checking input values which is DoCheck's use case when reading the docs.
export class ButtonGroupComponent implements DoCheck {
constructor(private _elementRef: ElementRef) {}
ngDoCheck() {
if (this.equalWidth) {
let buttons = this._elementRef.nativeElement.getElementsByTagName('button');
let widest = 0;
for (var button of buttons) {
if (button.getBoundingClientRect().width > widest) {
widest = button.getBoundingClientRect().width;
}
}
for (var button of buttons) {
button.style.width = widest + 'px';
}
}
}
}
Using AfterViewInit seems to do the trick, it only execute once resulting in the correct behavior and correct values. It's as simple as swapping DoCheck and ngDoCheck for AfterViewInit and ngAfterViewInit:
export class ButtonGroupComponent implements AfterViewInit {
constructor(private _elementRef: ElementRef) {}
ngAfterViewInit() {
if (this.equalWidth) {
let buttons = this._elementRef.nativeElement.getElementsByTagName('button');
let widest = 0;
for (var button of buttons) {
if (button.getBoundingClientRect().width > widest) {
widest = button.getBoundingClientRect().width;
}
}
for (var button of buttons) {
button.style.width = widest + 'px';
}
}
}
}
Hope this helps someone else doing DOM manipulation.

How do I update a value in an AngularJS (with TypeScript) controller when a value on an injected service updates

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
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++;
}
}
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++;
}
}
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.
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).
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.

Categories

Resources