firing $onChanges hook when collection is updated from the service in angularjs - angularjs

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.

Related

Angular 1.5 Components + ng-model $formatters and $parsers

I would like to know how to use $formatters and $parsers with angular 1.5 components. Can someone post an example.
Or is there something similar that I can use.
The following is an example of a component called example. This takes in a object that contains firstName and secondName. It then displays a combination of the firstName and secondName. If the object changes from the outside the formatter will fire followed by the render. If you want to trigger a change from the inside, you need to call this.ngModel.$setViewValue(newObject) and this would trigger the parser.
class example {
/*#ngInject*/
constructor() {}
// In the post link we need to add our formatter, parser and render to the ngmodel.
$postLink() {
this.ngModel.$formatters.push(this.$formatter.bind(this));
this.ngModel.$parsers.push(this.$parser.bind(this));
this.ngModel.$render = this.$render.bind(this);
}
// The formatter is used to intercept the model value coming in to the controller
$formatter(modelValue) {
const user = {
name: `${modelValue.firstName} ${modelValue.secondName}`
};
return user;
}
// The parser is used to intercept the view value before it is returned to the original source
// In this case we want to turn it back to it's original structure what ever that may be.
$parser(viewValue) {
// We know from out formatter that our view value will be an object with a name field
const namesParts = viewValue.name.split(' ');
const normalisedUser = {
firstName: namesParts[0],
secondName: namesParts[1],
};
return normalisedUser;
}
// This will fire when ever the model changes. This fires after the formatter.
$render() {
this.displayName = this.ngModel.$viewValue.name;
}
}
class ExampleComponent
{
bindings = {};
controller = Example;
require = {
ngModel: 'ngModel',
};
}
component('example', new ExampleComponent());
// Template for example component
<span>
{{ $ctrl.displayName }}
</span>
// Using the above component somewhere
<example ng-model="userModel"></example>

Angular2 ngOnChanges clone #Input array

I'm using a DashboardComponent that gets the data from my DashboardService. This Component then passes my array of objects to my form component.
(Plunkr link at bottom of post)
DashboardComponent.ts
private bottleArray: Bottle[] = [];
ngOnInit() {
// Get bottle types from service to the form needing them
this.dashboardService.getBottleTypesAndNames()
.subscribe(bottlesData => {
bottlesData.forEach(bottle => {
// Convert to Bottle type
let bottleObject: Bottle = new Bottle(bottle.bottleTypeId, bottle.bottleName);
this.bottleArray.push(bottleObject);
});
});
}
DashboardComponent.html
<ct-create-order-form [bottleArray]="bottleArray"> </ct-create-order-form>
I did it that way so that my form components linked to my Dashboard won't be doing any call to my service.
I'm trying to clone my #Input so that my data updated from the form is not linked to my parent component (Dashboard), but I can't seem to do it... See code below :
CreateOrderFormComponent.ts
export class CreateOrderFormComponent implements OnChanges {
#Input() private bottleArray: Bottle[];
constructor() { }
private clonedBottleArray: BottleCommand[];
ngOnChanges(changes) {
if (changes.bottleArray) {
let test: BottleCommand[] = changes.bottleArray.currentValue;
// Cloning
console.log(test); // Array of 6 Bottles
this.clonedBottleArray = [...test];
console.log(this.clonedBottleArray); // Empty Array
this.clonedBottleArray = Array.from(test);
console.log(this.clonedBottleArray); // Empty Array
this.clonedBottleArray = test.slice();
console.log(this.clonedBottleArray); // Empty Array
this.clonedBottleArray = test;
console.log(this.clonedBottleArray); // Array of 6 bottles
}
}
Is there any way to achieve what I am doing ? I don't understand why I can't clone my Input when I get the data ?
From this Youtube video made by AngularConnect, he is doing the exact same except that he is manipulating an Object, and I'm manipulating an Array of Objets.
https://youtu.be/-nsedZwvl9U?t=12m22s
EDIT : After creating a Plunkr, this seems to be working correctly in there.
https://plnkr.co/edit/js1vl0fcgOKtQNqXsWTL?p=preview
EDIT 2 : At the ngOnInit() from my DashboardComponent, if I mock the data, it is cloned correctly in my child component.
Looks like angular OnChange not firing due to it specific way of checking, here's brief explanation from this answer:
During change detection, when Angular checks components' input properties for change, it uses (essentially) === for dirty checking. For arrays, this means the array references (only) are dirty checked. Since the rawLapsData array reference isn't changing, ngOnChanges() will not be called.
And in your example, you're .pushing bottles in bottleArray, so OnChange doesn't fire on the same array reference.
To get the changes, you could use DoCheck:
ngDoCheck() {
console.log(this.bottleArray);
this.clonedBottleArray = [...this.bottleArray].slice(0, 4);
console.log(this.clonedBottleArray);
}
it will fire when you push new values to the bottleArray. Working plunker here.

Displaying data from Firebase in React without arrays

I am new to both React and Firebase. I struggled a bit to get data from the database, even though the instructions on the Firebase website were pretty straightforward.
I managed to print data in the view by using this code:
Get data from DB and save it in state:
INSTRUMENTS_DB.once('value').then(function(snapshot) {
this.state.instruments.push(snapshot.val());
this.setState({
instruments: this.state.instruments
});
From Firebase, I receive and Object containing several objects, which correspond to the differen instruments, like shown in the following snippet:
Object {
Object {
name: "Electric guitar",
image: "img/guitar.svg"
}
Object {
name: "Bass guitar",
image: "img/bass.svg"
}
// and so on..
}
Currently, I print data by populating an array like this:
var rows = [];
for (var obj in this.state.instruments[0]) {
rows.push(<Instrument name={this.state.instruments[0][obj].name}
image={this.state.instruments[0][obj].image}/>);
}
I feel like there's a better way to do it, can somedody give a hint? Thanks
I user firebase a lot and mu solution is little ES6 helper function
const toArray = function (firebaseObj) {
return Object.keys(firebaseObj).map((key)=> {
return Object.assign(firebaseObj[key], {key});
})
};
I also assign the firebase key to object key property, so later I can work with the keys.
The native map function only works for arrays, so using directly it on this object won't work.
What you can do instead is:
Call the map function on the keys of your object using Object.keys():
getInstrumentRows() {
const instruments = this.state.instruments;
Object.keys(instruments).map((key, index) => {
let instrument = instruments[key];
// You can now use instrument.name and instrument.image
return <Instrument name={instrument.name} image={instrument.image}/>
});
}
Alternatively, you can also import the lodash library and use its map method which would allow you to refactor the above code into:
getInstrumentRowsUsingLodash() {
const instruments = this.state.instruments;
_.map(instruments, (key, index) => {
let instrument = instruments[key];
// You can now use instrument.name and instrument.image
return <Instrument name={instrument.name} image={instrument.image}/>
});
}
Side note:
When you retrieve you data from Firebase you attempt to update the state directly with a call on this.state.instruments. The state in React should be treated as Immutable and should not be mutated with direct calls to it like push.
I would use map function:
_getInstrumentRows() {
const instruments = this.state.instruments[0];
if (instruments) {
return instruments.map((instrument) =>
<Instrument name={instrument.name}
image={instrument.image}/>);
}
}
In your render() method you just use {_getInstrumentRows()} wherever you need it.

Angular2 dont fire changeDetection after click

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;
}
}

How can I set nested array values in meteor publish function?

I have two collection "contents" and "units". In the content collection is a field "unitID" which refers to the unit-collection. In the meteor publish function I want to add the unit type name of all new created contents:
Meteor.publish("contents", function () {
var self = this;
var handle = Contents.find().observe({
changed: function(contentdoc, contentid) {
var UnitName = Units.findOne({_id: contentdoc.unittypeid }, {fields: {type: 1}});
self.set("contents", contentid, {'content.0.typename': UnitName});
self.flush();
}
});
}
This works but it creates a new attribut "content.0.UnitName" instead of inserting the attribute "UnitName" in the first element of the content array:
[
{
_id:"50bba3ca8f3d1db27f000021",
'content.0.UnitName':
{
_id:"509ff643f3a6690c9ca5ee59",
type:"Drawer small"
},
content:
[
{
unitID:"509ff643f3a6690c9ca5ee59",
name: 'Content1'
}
]
}
]
What I want is the following:
[
{
_id:"50bba3ca8f3d1db27f000021",
content:
[
{
unitID:"509ff643f3a6690c9ca5ee59",
name: 'Content1',
UnitName:
{
_id:"509ff643f3a6690c9ca5ee59",
type:"Drawer small"
}
}
]
}
]
What am I doing wrong?
this.set within Meteor.publish only works on the top-level properties of an object, meaning it doesn't support Mongo-style dotted attributes. You'll have to call set with the entire new value of the contents array.
Caveat: What I am about to say is going to change in a future release of Meteor. We're currently overhauling the custom publisher API to make it easier to use, but in a way that breaks back-compatibility.
That said...
It looks like what you're trying to do is build a server-side join into the published collection "contents". Here, for reference, is the current code (as of 0.5.2) that publishes a cursor (for when your publisher returns a cursor object):
Cursor.prototype._publishCursor = function (sub) {
var self = this;
var collection = self._cursorDescription.collectionName;
var observeHandle = self._observeUnordered({
added: function (obj) {
sub.set(collection, obj._id, obj);
sub.flush();
},
changed: function (obj, oldObj) {
var set = {};
_.each(obj, function (v, k) {
if (!_.isEqual(v, oldObj[k]))
set[k] = v;
});
sub.set(collection, obj._id, set);
var deadKeys = _.difference(_.keys(oldObj), _.keys(obj));
sub.unset(collection, obj._id, deadKeys);
sub.flush();
},
removed: function (oldObj) {
sub.unset(collection, oldObj._id, _.keys(oldObj));
sub.flush();
}
});
// _observeUnordered only returns after the initial added callbacks have run.
// mark subscription as completed.
sub.complete();
sub.flush();
// register stop callback (expects lambda w/ no args).
sub.onStop(function () {observeHandle.stop();});
};
To build a custom publisher that is joined with another table, modify the added callback to:
check if the added object has the key you want to join by
do a find in the other collection for that key
call set on your subscription with the new key and value you want to be published, before you call flush.
Note that the above is only sufficient if you know the key you want will always be in the other table, and that it never changes. If it might change, you'll have to set up an observe on the second table too, and re-set the key on the sub in the changed method there.

Resources