Angular2 doesn't trigger the ChangeDetection after a click event. The code snippets below are to get the data from one component to another.
onClickEvent
(click)="$event.preventDefault(); setApartmentObject(FlatObject)";
ApartmentOverviewComponent
constructor(private _apart:ApartmentService) {}
setApartmentObject(flat:ApartmentObject) {
this._apart.setApartmentDetails(flat);
}
ApartmentService
Injectable()
export class ApartmentService {
apartmentDetails:ApartmentObject
getApartmentDetails():Observable<ApartmentObject> {
return Observable.create((observer) => {
observer.next(this.apartmentDetails);
observer.complete();
});
}
setApartmentDetails(value:ApartmentObject) {
this.apartmentDetails = value;
}
}
ApartmentDetailComponent
constructor(private _apart:ApartmentService)
get apartmentDetails() {
this._apart.getApartmentDetails().subscribe(data => {
this._apartmentDetails = data;
});
return this._apartmentDetails;
}
In the HTML file
<p><strong>{{apartmentDetails.name || 'Musterwohnung'}}</strong></p>
I also tried to fix this problem with an eventemitter, but without success. Only the following dirty fix works:
constructor(private _ref:ChangeDetectorRef) {
this._ref.detach();
setInterval(() => {
this._ref.detectChanges();
}, 300);
}
There are some issues with your code that actually prevent the value from being read.
First of all—in your service—when you set the value, you just do it on the service's instance, instead of feeding it to the observable object. The observable just can't know that value has changed, so it won't emit the change (next) event. This is why the ApartmentOverviewComponent. setApartmentObject() does nothing. To actually feed the observable with data, you need to use a Subject.
In the ApartmentDetailComponent, in this simple scenario (where data is always synchronously provided), you could get the value in the way you try it. But, as mentioned before, the data won't ever change. It's also needles to store the data on the component's instance's _apartmentDetails field. You could use the observable in your template.
The working implementation is like that:
#Injectable()
class ApartmentService {
// BehaviorSubject is a type of an Observable that can be manually fed with data
// and returns it's last value to any subscriber.
apartmentDetails = new BehaviorSubject<ApartmentObject>({name: 'Musterwohnung'});
// Instead of using a property of the service, just inform the
// subject about knew data and let it spread the change for you.
setApartmentDetails(value: ApartmentObject) {
this.apartmentDetails.next(value);
}
}
#Component({
selector: 'overview-cmp',
// Side note: you don't need to .preventDefault() here.
template: `<a (click)="setApartmentObject({name: 'Shiny Aparament'})">click</a>`
})
class ApartmentOverviewComponent {
constructor(private apartService: ApartmentService) {}
// Works same as before.
setApartmentObject(flat: ApartmentObject) {
this.apartService.setApartmentDetails(flat);
}
}
#Component({
selector: 'details-cmp',
// Use the 'async' pipe to access the data stored in an Observable
// object. Also, to secure the code, use '?' to safely access the property.
template: `<p><strong>{{(details | async)?.name}}</strong></p>`
})
class Apartament {
// This is the observable with data.
details: Observable<ApartmentObject>;
constructor(private apartService: ApartmentService) {}
// When component initialises, assign the observable data from the service
ngOnInit() {
this.details = this.apartService.apartmentDetails;
}
}
Related
I have an object as follows which comes through #Input.
#Input() data;
//**
{
"class_a":["John","Harr y"],
"class_b":["Joseph","Phlip","David"],
"class_c":[]
}
**//
I need to detect the changes if data added or removed in class_a or class_b but im only getting change detection if values of objects are string.
Since the keys in my object are dynamic i couldn't iterate the object and create Iterable differs.
Is there anyway to detect changes of array inside the object.
My Implementation:
constructor(private differs: KeyValueDiffers) {
this.keyValueDiffer = differs.find({}).create();
}
ngDoCheck() {
let changes = this.keyValueDiffer.diff(this.data[this.component.factedBindKey]);
if (changes) {
console.log('Changes detected');
}
}
you can test like this
constructor(private cd: ChangeDetectorRef) {
}
ngOnChanges() {
let actualData =this.data
this.mymethod(actualData);
}
and call this line where you want to access that actual data like this
mymethod(data){
this.cd.detach();
//write main logic
}
I am trying to make $onChanges hook work by using immutable way.
Chat Service
class ChatService {
constructor() {
this.collection = {
1: [
{
chat: 'Hi',
},
{
chat: 'Hello',
},
{
chat: 'How are you?',
},
],
};
}
getCollection() {
return this.collection;
}
getChatById(id) {
return this.collection[id];
}
addChat(id, chat) {
// this.collection[id].push(chat);
this.collection[id] = this.collection[id].concat(chat);
}
}
Chat Component
const Chat = {
bindings: {},
template: `<chat-list chats="$ctrl.chats" add-msg="$ctrl.addMsg(chat)"></chat-list>`,
// template: `<chat-list chats="$ctrl.chats[$ctrl.id]" add-msg="$ctrl.addMsg(chat)"></chat-list>`,
controller: class Chat {
constructor(ChatService) {
this.ChatService = ChatService;
this.id = 1;
// if i get the all the chat collection by
// this.chats = ChatService.getCollection()
// and then use like above in the commented out template,
// and it works and triggers $onChanges
this.chats = ChatService.getChatById(this.id);
}
addMsg(msg) {
this.ChatService.addChat(this.id, { chat: msg });
}
},
};
Chat List Component
const ChatList = {
bindings: {
chats: '<',
addMsg: '&',
},
template: `
<div>
<li ng-repeat="chat in $ctrl.chats">{{chat.chat}}</li>
<form ng-submit="$ctrl.addMsg({chat: chatmodel})">
<input ng-model="chatmodel">
</form>
</div>
`,
controller: class ChatList {
$onChanges(changes) {
console.log(changes);
if (changes.chats && !changes.chats.isFirstChange()) {
// this.chats = changes.chats.currentValue;
}
}
},
};
However, $onChanges hook doesn't fire. I know that in order to make the $onChanges fire, need to break the reference of binding chats in chat-list component from the chat component.
Also I could re-fetch the chats after adding on the addMsg method, it would work and trigger $onChanges but if the msg was from the another user and lets say if I was using Pusher service, it would only update the chats collection on the Chat Service not the chat-list component.
One way $onChanges seems to fire is when I get all the chat collection and then use ctrl.id to get particular chats when passing via the bindings like <chat-list chats="$ctrl.chats[$ctrl.id]" instead of <chat-list chats="$ctrl.chats. However, this will update chat list without doing anything on the $onChanges.
Ideally, I would like to update the chat list on the view by <chat-list chats="$ctrl.chats and then using the currentValue from the $onChanges hook and not use like $watch and $doCheck. I am not sure how to do it. Any help is appreciated. Thanks and in advance.
Here's very basic example of it on the plunkr.
Let's walk trough what your code is doing for a minute to ensure we understand what's going wrong:
The constructor in ChatServices creates a new object in memory (Object A), this object has a property 1 which holds an array in memory (Array 1)
constructor() {
this.collection = {
1: [
{
chat: 'Hi',
},
{
chat: 'Hello',
},
{
chat: 'How are you?',
},
],
};
}
In your component's constructor, you use the ChatService to retrieve Array 1 from memory and store it in the this.chats property from your component
this.chats = ChatService.getChatById(this.id);
So currently, we have two variables pointing to the same array (Array 1) in memory: The chats property on your component and the collection's 1 property in the ChatService.
However, when you add a message to the ChatService, you are using the following:
addChat(id, chat) {
this.collection[id] = this.collection[id].concat(chat);
}
What this is doing is: It updates collection's 1 property to not point towards Array 1, but instead creates a new array by concatenating both the current Array 1 and a new message, store it in memory (Array 2) and assign it to collection[id].
Note: This means the Object A object's 1 property also points to Array 2
Even tho the collection's 1 property has been updated properly when it comes to immutability, the chats property on your component is still pointing towards Array 1 in memory.
There's nothing indicating it should be pointing to Array 2.
Here's a simple example demonstrating what's happening:
const obj = { 1: ['a'] };
function get() {
return obj['1'];
}
function update() {
obj['1'] = obj['1'].concat('b');
}
const result = get();
console.log('result before update', result );
console.log('obj before update', obj['1']);
update();
console.log('result after update', result );
console.log('obj after update', obj['1']);
As you can see in the above snippet, pointing obj['1'] towards a new array doesn't change the array result points to.
This is also why the following is working correctly:
One way $onChanges seems to fire is when I get all the chat collection
and then use ctrl.id to get particular chats when passing via the
bindings like <chat-list chats="$ctrl.chats[$ctrl.id]" instead of
<chat-list chats="$ctrl.chats.
In this case you are storing a reference to Object A. As mentioned above, the 1 property on the ChatService's collection is updated correctly, so this will reflect in your component as it's also using that same Object A.
To resolve this without using the above way (which is, passing Object A to your component), you should ensure the component is aware of the changes made to Object A (as it can not know this when not having access to it).
A typical way these kind of things are done in Angular (I know this is AngularJS, but just pointing out how you can resolve this in a way Angular would do and works fine with Angular JS) is by using RXjs and subscribe to the chats changes in your component.
I would like to bind an input value to the model using getter and setters. In this way i can prevent and/or manipulate the input's value while writing inside it.
For example i want the prevent numbers inside an input box. So, if write 'abc' all is ok, then if I start writing a number nothing should happen (to the model and to the input's value). The issue is that with the following code i'm able to write anything inside the input box (but the model it's correct). This means that the input box value is not really representing my model.
NOTE: The reason beyond this questions is that I want to use my models to validate forms, preventing for example specific characters. I would like to not use reactive forms as i want to keep my validations inside my models not components. Also note that in a real scenario i would have a UserModel class with inside name and other fields with their validations.
#Component({
selector: 'my-app',
template: `
<div>
<h2><input type="text" [(ngModel)]="name"> {{name}}</h2>
</div>
`,
})
export class App {
_name:string = 'ss';
constructor() {
}
// In real scenario those 2 methods are in a separate class UserModel
get name() {
return this._name;
}
set name() {
if ((new RegExp(/^[a-zA-Z]*$/).test(val))) {
this._name = val;
}
}
}
If you manipulate the value in the setter, this can cause issues with change detection, so that ngModel doesn't pick up the changes and doesn't update the <input>
To work around you can use
export class App {
_name:string = 'ss';
constructor(private cdRef:ChangeDetectorRef) {}
get name() {
return this._name;
}
set name(value:String) {
this._name = value + 'x';
this.cdRef.detectChanges()
}
}
if you reset the value to the previous value, you might need to pass an artificial different value first, otherwise change detection won't detect a change and even detectChanges() won't update the input.
set name(value:String) {
var oldVal = this._name;
this._name = null;
this.cdRef.detectChanges()
this._name = oldVal;
this.cdRef.detectChanges()
}
Based on #Günter Zöchbauer answer i made a workaround. It's not definitive and could be more abstract, but for now it's ok.
export class App implements OnInit {
#Input() userModel: UserModel = null;
public _vm;
constructor(private _changeDetectionRef: ChangeDetectorRef) {
}
/**
* Initalize view model, it's important to keep names specular
*/
ngOnInit() {
this._vm = {
name: this.userModel.name,
surname: this.userModel.surname,
};
}
/**
* Helper for avoid detectchanges inside the modal, and reduce boilerplate. We could also ad an interface/type of the possibile field value, ie type fieldT= 'name' | 'surname';
* #param field
* #param val
*/
protected updateModel(field, val: string): void {
this._vm[field] = null;
this._changeDetectionRef.detectChanges();
this.userModel[field] = val;
this._vm[field] = this.userModel[field];
this._changeDetectionRef.detectChanges();
}
}
In userModel:
....
public get name(): string {
return this.name';
}
public set name(val: string) {
if ((new RegExp(/^[a-zA-Z]*$/).test(val))) {
this.name = val;
}
}
In template:
<input type="text" name="userName" [ngModel]="_vm.name" (ngModelChange)="updateModel('name', $event)">
You can use (ngModelChange) and [ngModel] to test the content of your model upon change.
As you can see in this Plunker the model wont change if it is not valid.
#Component({
selector: 'my-app',
template: `
<div>
<h2><input #input type="text" [ngModel]="name" (ngModelChange)='valid(input.value)'> {{name}}</h2>
</div>
`,
})
export class App {
name:string = 'ss';
constructor() {
}
valid(value){
if(value){ //<--- Your test here
this.name = value;
}
}
}
I'm working on using a kendo inside of an angular 2 project.
Getting the widget set up correctly is no problem:
ngOnInit() {
let options = inputsToOptionObject(KendoUIScheduler, this);
options.dataBound = this.bound;
this.scheduler = $(this.element.nativeElement)
.kendoScheduler(options)
.data('kendoScheduler');
}
When that runs, the plugin modifies the DOM (and, to my knowleged, without modifiying the shadow DOM maintained by angular2). My issue is that if I want to use a component anywhere inside of the plugin, like in a template, Angular is unaware of it's existence and won't bind it.
Example:
public views:kendo.ui.SchedulerView[] = [{
type: 'month',
title: 'test',
dayTemplate: (x:any) => {
let date = x.date.getDate();
let count = this.data[date];
return `<monthly-scheduler-day [date]="test" [count]=${count}"></monthly-scheduler-day>`
}
}];
The monthly-scheduler-day class:
#Component({
selector: 'monthly-scheduler-day',
template: `
<div>{{date}}</div>
<div class="badge" (click)=dayClick($event)>Available</div>
`
})
export class MonthlySchedulerDayComponent implements OnInit{
#Input() date: number;
#Input() count: number;
constructor() {
console.log('constructed');
}
ngOnInit(){
console.log('created');
}
dayClick(event){
console.log('clicked a day');
}
}
Is there a "right" way to bind these components inside of the markup created by the widget? I've managed to do it by listening for the bind event from the widget and then looping over the elements it created and using the DynamicComponentLoader, but it feels wrong.
I found some of the details I needed in this thread: https://github.com/angular/angular/issues/6223
I whipped this service up to handle binding my components:
import { Injectable, ComponentMetadata, ViewContainerRef, ComponentResolver, ComponentRef, Injector } from '#angular/core';
declare var $:JQueryStatic;
#Injectable()
export class JQueryBinder {
constructor(
private resolver: ComponentResolver,
private injector: Injector
){}
public bindAll(
componentType: any,
contextParser:(html:string)=>{},
componentInitializer:(c: ComponentRef<any>, context: {})=>void):
void
{
let selector = Reflect.getMetadata('annotations', componentType).find((a:any) => {
return a instanceof ComponentMetadata
}).selector;
this.resolver.resolveComponent(componentType).then((factory)=> {
$(selector).each((i,e) => {
let context = contextParser($(e).html());
let c = factory.create(this.injector, null, e);
componentInitializer(c, context);
c.changeDetectorRef.detectChanges();
c.onDestroy(()=>{
c.changeDetectorRef.detach();
})
});
});
}
}
Params:
componentType: The component class you want to bind. It uses reflection to pull the selector it needs
contextParser: callback that takes the existing child html and constructs a context object (anything you need to initialize the component state)
componentInitializer - callback that initializes the created component with the context you parsed
Example usage:
let parser = (html: string) => {
return {
date: parseInt(html)
};
};
let initer = (c: ComponentRef<GridCellComponent>, context: { date: number })=>{
let d = context.date;
c.instance.count = this.data[d];
c.instance.date = d;
}
this.binder.bindAll(GridCellComponent, parser, initer );
Well your solution works fine until the component needs to change its state and rerender some stuff.
Because I haven't found yet any ability to get ViewContainerRef for an element generated outside of Angular (jquery, vanilla js or even server-side)
the first idea was to call detectChanges() by setting up an interval. And after several iterations finally I came to a solution which works for me.
So far in 2017 you have to replace ComponentResolver with ComponentResolverFactory and do almost the same things:
let componentFactory = this.factoryResolver.resolveComponentFactory(componentType),
componentRef = componentFactory.create(this.injector, null, selectorOrNode);
componentRef.changeDetectorRef.detectChanges();
After that you can emulate attaching component instance to the change detection cycle by subscribing to EventEmitters of its NgZone:
let enumerateProperties = obj => Object.keys(obj).map(key => obj[key]),
properties = enumerateProperties(injector.get(NgZone))
.filter(p => p instanceof EventEmitter);
let subscriptions = Observable.merge(...properties)
.subscribe(_ => changeDetectorRef.detectChanges());
Of course don't forget to unsubscribe on destroy:
componentRef.onDestroy(_ => {
subscriptions.forEach(x => x.unsubscribe());
componentRef.changeDetectorRef.detach();
});
UPD after stackoverflowing once more
Forget all the words above. It works but just follow this answer
I'm using a TypeScript class to define a controller in AngularJS:
class TrialsCtrl {
constructor(private $scope: ITrialsScope, private ResourceServices: ResourceServices) {
this.loadTrials();
}
loadTrials() {
console.log("TrialsCtrl:", this);
this.Trial.query().then((result) => {
this.$scope.Trials = result;
});
}
remove(Trial: IRestTrial) {
this.ResourceServices.remove(Trial, this.loadTrials);
}
}
angular.module("app").controller("TrialsCtrl", TrialsCtrl);
I'm refactoring common controller methods into a service.
class ResourceServices {
public remove(resource, reload) {
if (confirm("Are you sure you want to delete this?")) {
resource.remove().then(() => {
reload();
});
}
}
}
angular.module("app").service("ResourceServices", ResourceServices);
The console log shows that this is referencing the window context when I want it to be TrialsCtrl. My problem is that the reload() method needs to run in the context of TrialsCtrl, so that it can access this.Trial and this.$scope. How can I tell the reload() method to set this as the TrialsCtrl? Or is there some other workaround I should be using for this kind of thing?
Have you tried:
this.ResourceServices.remove(Trial, this.loadTrials.bind(this));
or
this.ResourceServices.remove(Trial, () => this.loadTrials());
For methods that are supposed to be passed as callbacks (as with this.loadTrials) it is preferable to define them as arrows,
loadTrials = () => { ... }
So they keep the context whether Function.prototype.bind is used or not.
Alternatively, a decorator may be used on the method (like core-decorators #autobind) to bind a method while still defining it on class prototype:
#autobind
loadTrials() { ... }