I've run into big performance problem when I tried to implement codemirror, firstly I used ng2-codemirror but it's killed my application which has only list of scripts and codemirror component. I found out that creating instance (by instance i mean CodeMirror.fromTextArea(...)) should be run outside angular so I copied component from ng2-codemirror (code attached below) to my project and wrap init method with NgZone.runOutsideAngular, it helps but just a bit. So I've started profiling my angular project with Javascript profiler in chrome debugger and vanilla js codemirror with the same code to edit. My small research shows me that without zone.js codemirror is executing same function with about 8% of my CPU resources however with zone.js in angular project it takes 48%. So I've started to completly detach zone from codemirror component, So I tried with ChangeDetectionRef.detach() but the zone still is attached to codemirror (checked with JS profiler again).
Summary:
runOutsideAngular does not help to detach codemirror source code from zone, same is for detaching changeDetector for codemirror component
Code for my modified codemirror component
export class MpCodemirrorComponent implements AfterViewInit, OnDestroy {
#Input() config;
#Output() change = new EventEmitter();
#Output() focus = new EventEmitter();
#Output() blur = new EventEmitter();
#Output() cursorActivity = new EventEmitter();
#ViewChild('host') host;
#Output() instance = null;
_value = '';
private changeSubject: Subject<any> = new Subject<any>();
constructor(
private ngZone: NgZone,
private changeDetector: ChangeDetectorRef
) {
this.changeDetector.detach();
}
get value() { return this._value; }
#Input() set value(v) {
if (v !== this._value) {
this._value = v;
this.onChange(v);
}
}
ngOnDestroy() {
}
ngAfterViewInit() {
this.changeDetector.detach();
this.config = this.config || {};
this.codemirrorInit(this.config);
this.changeSubject
.debounceTime(500)
.distinctUntilChanged()
.subscribe(change => this.updateValue(change));
}
codemirrorInit(config) {
this.ngZone.runOutsideAngular(() => {
this.instance = CodeMirror.fromTextArea(this.host.nativeElement, config);
this.instance.setValue(this._value);
this.instance.on('change', () => {
this.changeSubject.next(this.instance.getValue());
// this.updateValue(this.instance.getValue());
});
this.instance.on('focus', (instance, event) => {
this.focus.emit({instance, event});
});
this.instance.on('cursorActivity', (instance) => {
this.cursorActivity.emit({instance});
});
this.instance.on('blur', (instance, event) => {
this.blur.emit({instance, event});
});
})
}
updateValue(value) {
this.value = value;
this.onTouched();
this.change.emit(value);
}
writeValue(value) {
this._value = value || '';
if (this.instance) {
this.instance.setValue(this._value);
}
}
onChange(_) {}
onTouched() {}
registerOnChange(fn) { this.onChange = fn; }
registerOnTouched(fn) { this.onTouched = fn; }
}
I've run out of ideas how to completly detach zone for 3rd party library, any help is appreciated.
P.S. Don't mind doubled changeDetector.detach().
Related
I have a component that i send to MdDialog(Angular Material Dialog in my custom service.ts)
dialogRef = this.dialog.open(component, config);
And when I change a public property of this component by componentInstance like that:
dialogRef.componentInstance.task = task;
Angular shows me an error:
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'dialog'. It seems like the view has been created after its parent and its children have been dirty checked. Has it been created in a change detection hook ?
Full code of open-modal.service.ts
#Injectable()
export class TasksPopupService {
constructor(
private dialog: MdDialog,
private router: Router,
private tasksService: TasksService
) { }
public open(component: any, id?: string) {
if (id) {
this.tasksService.find(id)
.subscribe(task => {
this.bindDialog(component, task);
});
} else {
this.bindDialog(component, new Task());
}
}
bindDialog(component, task: Task) {
let dialogRef;
let config = new MdDialogConfig();
config.height = '80%';
config.width = '70%';
dialogRef = this.dialog.open(component, config);
dialogRef.componentInstance.task = task;
dialogRef.afterClosed().subscribe(res => {
this.router.navigate([{ outlets: { popup: null } }], { replaceUrl: true });
});
return dialogRef;
}
}
But an error occured only if id is undefined (in ELSE block) I think it's because of this.tasksService.find return Observable (async), and block ELSE is not async. But I'm not sure.
I has some confuse becouse error eccured in MdContainer of Angular Material.
If i get data from server it's need some time, but when i pass a new object it's occur fast and change detection is not finished if i understend right.
Also, it's not parent/child component and lifecycle hooks maybe not works as we expect.
I found solution, but it's not right. Just fast solution.
if (id) {
this.tasksService.find(id)
.subscribe(task => {
this.bindDialog(component, task);
});
} else {
Observable.of(new Task()).delay(300).subscribe(task => {
this.bindDialog(component, task);
});
}
I use delay for change detection has finished and error will not throw.
I'm trying to create a small Directive to capture the windows global-keyup and then invoke a callback, so I basically captue the global window in a service and the keyup on my Directive:
export class EnterActivationDirective implements OnInit {
private _enterClicked: Action;
#Input() public set enterClicked(action: Action) {
this._enterClicked = action;
}
constructor(public el: ElementRef, public windowWrapperService: WindowWrapperService) {
}
ngOnInit() {
this.windowWrapperService.nativeWindow.onkeyup = this.onWindowKeyUp.bind(this);
}
private onWindowKeyUp(event: any) {
if (event.code === 'Enter' && this._enterClicked) {
this._enterClicked();
}
}
}
The Service and Action-Type aren't that interesting, since the Service just passes the native window and the Action-Type is a generic Callback without any parameters or return-value.
The logic itself works, but I get some weird effects regarding the binding to the action. So, one of my other Components registers to the Directive:
<div appEnterActivation [enterClicked]="onKeyUp.bind(this)">
<div>
... Amended
Which then triggers a search-operation:
public search(): void {
this.searchInProgress = true;
const param = this.createSearchParams();
this.searchStarted.emit(param);
this.timeReportEntryApiService.searchTimeReportEntries(param)
.then(f => {
const newObjects = ArrayMapper.MapToNewObjects(f, new TimeReportEntry());
this.searchFinished.emit(newObjects);
this.searchInProgress = false;
}).catch(f => {
this.searchInProgress = false;
throw f;
});
}
public get canSearch(): boolean {
return this.form.valid && !this.searchInProgress;
}
public onKeyUp(): void {
debugger ;
if (this.canSearch) {
this.search();
}
}
Not too much logic here, but if the search is started from the callback, it seems like the properties and functions are in place, but they are on some kind of different object:
The searchInProgress-property is set tu true, but on the second enter, it is false again
I have some animations and bindings in place, none of them are triggered
Since everything is working with a plain button, I'm almost certain it kindahow has to do with the callback and the binding to this.
I researched a bit regarding this bind, but regarding this thread Use of the JavaScript 'bind' method it seems to be needed. I also tested without binding, but then the this is bound to the global window variable.
Why are you using an #Input? Angular made #Output for such a use case:
template:
<div appEnterActivation (enterClicked)="onEnter()"></div>
class:
export class EnterActivationDirective implements OnInit {
#Output()
public readonly enterClicked: EventEmitter<any> = new EventEmitter();
#HostBinding('document.keyup.enter')
onEnter(): void {
this.enterClicked.emit();
}
}
No need for difficult checks or wrappers :)
Since you are using TypeScript you can use arrow function, that manages this correctly.
public onKeyUp = () => {
debugger ;
if (this.canSearch) {
this.search();
}
}
In that case you can just setup the property binding as
[enterClicked]="onKeyUp"
I am building out an Angular 2 app using the Angular-CLI, after initially building a working version of the app using the non-CLI version of Angular 2. To my surprise, some code that wasn't a problem in my non-CLI app version HAS been a problem with my Angular-CLI version. All that said, I've resolved everything except for one final error I am stuck on.
This is the error message I'm getting:
Uncaught Error: Module build failed: Error:
/Users/fdr/Documents/rds/rds/cli-rds/src/app/ui/generate-field.component.ts
(340,48): Return type of public method from exported class has or is
using private name 'Hint'.)
And here is the problem file causing the error:
import { Component, Input, Output, EventEmitter, OnInit, AfterViewInit, ElementRef, ViewChild } from '#angular/core';
import { EventHandler } from '../app.event-handler';
import '../app.utils';
#Component({
selector: 'app-generate-field',
templateUrl: 'app/ui/generate-field.component.html',
styleUrls: ['app/ui/generate-field.component.css']
})
export class GenerateField extends EventHandler
{
public get hasFocus(): boolean
{
return this._hasFocus;
}
#Input() delay: number = 300;
#ViewChild('inputField') private inputField: ElementRef;
#ViewChild('suggestionField') private suggestionField: ElementRef;
#Input() public value: string;
#Output() private valueChange: EventEmitter<string> = new EventEmitter<string>();
#Output() public keyup: EventEmitter<KeyboardEvent> = new EventEmitter<KeyboardEvent>();
#Output() public focus = new EventEmitter<KeyboardEvent>();
#Output() public blur = new EventEmitter<KeyboardEvent>();
private inlineSuggestion: string;
private suggestions: ISuggestion[];
#Input() public options: string[];
#Output() private optionsChange: EventEmitter<string[]> = new EventEmitter<string[]>();
private isDirty: boolean = false;
private _hasFocus: boolean = false;
constructor(myElement: ElementRef)
{
super();
this.defineObservableProperty('value');
this.defineObservableProperty('isDirty');
this.defineObservableProperty('suggestions');
this.defineObservableProperty('options');
this.addPropertyListener('isDirty', function ()
{
if (this.isDirty == false)return;
var delay = this.delay ? this.delay : 500;
var self = this;
setTimeout(function ()
{
self.updateSuggestions();
this.isDirty = false;
}.bind(this), delay);
}.bind(this));
this.addPropertyListener('value', (): void=>
{
this.valueChange.emit(this.value);
this.isDirty = true;
});
this.addPropertyListener('suggestions', (): void=>
{
this.updateInlineSuggestion();
});
this.addPropertyListener('options', ()=>
{
this.optionsChange.emit(this.options);
});
}
//--------------------------------------------------------
// Functions
//--------------------------------------------------------
/**
* Evaluates value and updates the list of suggestions
*/
public updateSuggestions(): void
{
// Update suggestions
this.suggestions = this.generateSuggestions(this.value);
}
/***
* Updates the inline suggestion that appears on the text field
*/
private updateInlineSuggestion(): void
{
// Clear inline if there are no suggestions
if (this.suggestions.length == 0)
{
this.inlineSuggestion = '';
return;
}
// Show first option inline
this.inlineSuggestion = this.suggestions[0].value;
var x = this.inputField.nativeElement.selectionStart;
var y = this.inputField.nativeElement.selectionEnd;
this.suggestionField.nativeElement.selectionStart = x;
this.suggestionField.nativeElement.selectionEnd = y;
this.inputField.nativeElement.selectionStart = x;
this.inputField.nativeElement.selectionEnd = y;
this.suggestionField.nativeElement.scrollLeft = x;
}
private onFocus(): void
{
this._hasFocus = true;
// Forward event
this.focus.emit();
}
private onBlur(): void
{
this._hasFocus = false;
// Forward event
this.blur.emit();
}
interface ISuggestion
{
word: string;
match: string;
value: string;
}
try to add "export interface ISuggestion" at the last part of your code so that ISuggestion is exported, too.
Try adding : any after your method. I encountered same problem, It solved after I added any.
I'm migrating from angular 1.x to 2.x but my brains still think in angular 1.x so sorry for silly questions.
What I need is to take some action when one of my scope variables component properties changes. I found a solution but I think there should be better solution
export class MyApp {
router: Router;
location: Location;
fixed: boolean = true;
private set isFixed(value:boolean) {
this.fixed = value;
//TODO: look here
console.log('isFixed changed', value);
}
private get isFixed():boolean {
return this.fixed;
}
constructor(router: Router, location: Location) {
this.router = router;
this.location = location;
}
}
Look at the line console.log('isFixed changed', value); It's what I need and it's working. But I made it by declaring getter and setter, but isn't there a better solution to watch variables? Like in angular 1.x was $scope.$watch?
I think my component code should look like
export class MyApp {
router: Router;
location: Location;
isFixed: boolean = true;
//TODO: $watch for isFixed change {
console.log('isFixed changed', value);
// }
constructor(router: Router, location: Location) {
this.router = router;
this.location = location;
}
}
You might want to implement the OnChanges interface and implement the ngOnChanges() method.
This method is called whenever one of the components input or output binding value changes.
See also https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
Dart code example
#Input() bool fixed;
#override
void ngOnChanges(Map<String, SimpleChange> changes) {
print(changes);
}
You might find this answer to Delegation: EventEmitter or Observable in Angular2 helpful (worked for me).
Essentially you could use a BehaviorSubject, which allows you to set an initial value for the property you're interested in, then subscribe to changes to that property wherever that service is injected.
e.g.
export class SomeService {
private fixed = new BehaviorSubject<boolean>(true); // true is your initial value
fixed$ = this.fixed.asObservable();
private set isFixed(value: boolean) {
this.fixed.next(value);
console.log('isFixed changed', value);
}
private get isFixed():boolean {
return this.fixed.getValue()
}
constructor(router: Router, location: Location) {
this.router = router;
this.location = location;
}
}
Then in a class (e.g. Component) that's interested in the fixed value:
export class ObservingComponent {
isFixed: boolean;
subscription: Subscription;
constructor(private someService: SomeService) {}
ngOnInit() {
this.subscription = this.someService.fixed$
.subscribe(fixed => this.isFixed = fixed)
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Update value:
export class Navigation {
constructor(private someService: SomeService) {}
selectedNavItem(item: number) {
this.someService.isFixed(true);
}
}
To auto get updated value by this service
NOTE: I tested it in Angular 9
my service file
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class SharedService {
private fixed= new BehaviorSubject<boolean>(false);
fixed$ = this.fixed.asObservable();
constructor() {}
updateFixedValue(value: boolean) {
this.fixed.next(value);
console.log('fixed changed', value);
}
}
Now you can get value in any component (within ngOnInit or anywhere you want) like below
NOTE: this value will be change automatically after update
this.sharedService.fixed$.subscribe(val=>{ this.isFixed = val; });
and you can update or set new value from any component like below
this.sharedService.updateFixedValue(your_boolean_value);
Thanks, I hope it's work for you.
See Angular2 Component Interaction (has code examples).
The short answer to your question is that it really just depends on what you are trying to do. Even then, there are multiple ways to do what you want to do even if it's not really intended for it. So, I think it's best if you just take a few minutes to look at their documentation about Component Interaction and Forms.
My personal preference is to use events when a property has changed. The ngOnChanges event can be used for this but I prefer to work with #Input and #Output, and form value changed events (Angular2 Forms).
Hope this helps and gives you a direction you want to take.
I think it is possible to use simple getter and setter for this purpose.
class Example {
private _variable: string = "Foo";
set variable(value: string) {
this._variable = value;
console.log("Change detected: ", this.variable);
}
get variable(): string {
return this._variable;
}
}
let example = new Example();
console.log(example.variable);
example.variable = "Bar";
console.log(example.variable);
And output will be:
Foo
Change detected: Bar
Bar
Below is typescript for an angular directive. The problem is with the injected service "datacontext". The debugger shows that the datacontext in the constructor is a WINDOW object, not the datacontext object that was created as a service. Therefore, in the scope.viewRecord function, the datacontext.cancelChanges() function is, obviously, undefined - - as its not part of a WINDOW object This is probably some strange scoping issue that I just don't get, but I'm at a loss as to how to debug this. Any insight would be most appreciated. Thanks.
module app {
export interface IEditButtonGroup {
...
}
export interface IEditButtonScope extends ng.IScope {
...
}
export class PrxEditButtonGroup implements IEditButtonGroup {
public static $inject: Array<string> = [
"datacontext"
];
constructor(
public datacontext: IDataContext, <---- datacontext HERE is typed as a WINDOW object
public directive: ng.IDirective = {}) {
directive.templateUrl = "app/directives/templates/editbuttongroup.html",
directive.restrict = 'E';
directive.link = (scope: IEditButtonScope, element, attr) => {
scope.isEditing = false;
scope.isAdding = false;
$("form.disabled").find("input:not(:disabled), select:not(:disabled), textarea:not(:disabled)").prop("disabled", true);
scope.editRecord = () => {
$("input, select, textarea").removeAttr("disabled");
scope.isEditing = true;
scope.afterEdit();
}
scope.viewRecord = (afterCancel: boolean) => {
datacontext.cancelChanges(); <--- HERE TOO! Debugger says datacontext = WINDOW object
scope.isEditing = scope.isAdding = false;
$("form.disabled").find("input:not(:disabled), select:not(:disabled), textarea:not(:disabled)").prop("disabled", true);
scope.afterAdd();
}
}
return <any>directive;
}
}
}
The error seems to be at the place where you register this directive. Make sure it is like :
mymodule.directive('prxEditButtonGroup',app.PrxEditButtonGroup )