Reputation: 3
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.
Upvotes: 0
Views: 512
Reputation: 1509
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's1
property also points toArray 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.
Upvotes: 1