How to "react" on data changes with RxJS? - javascript

RxJS beginner here: I have problems with saving and tracking data changes using RxJS. Say I structure my app in small views/widgets and every view/widget has its own state and should do things on data changes. How do I do that?
More concrete example. Let's say I have a widget called Widget and Widget has a title and button. The state should contain the title and the information if the button was already clicked. From reading the docs of RxJS it seems this would be a good starting point:
var widgetState = new Rx.Subject().startWith({
wasClicked: false,
title: 'foo'
});
Now I want to be notified if some data changes:
var widgetStateChanges = widgetState.subscribe(function(data) {
console.log('data: ', data);
// what do i do with the data here?
// i would like to merge the new data into the old state
});
widgetStateChanges.onNext({ title: 'bar' });
I listen to the changes, but I don't know how to save them. I would also like to do special things, if a certain data change happens. Something like this.
widgetStateChanges.filter(function(e) {
return e.wasClicked;
}).do(function(e) {
console.log('Do something because was clicked now.');
});
However I can't filter a subscription (widgetStateChanges), only a subject (widgetState).

Use a BehaviorSubject to track observable state:
var widgetState = new Rx.BehaviorSubject({ wasClicked: false, title: 'foo' });
// change state, probably in response to UI events
// Note we always set the full state, not just the "delta"
widgetState.onNext({ wasClicked: true, title: 'foo2' });
// example listening to title input field and updating state
// assumes rxjs-jquery
$("#title").onAsObservable("change").subscribe (function (ev) {
var oldState = widgetState.value;
var newTitle = $("#title").val();
// do not mutate the oldState object, instead clone it and change the title
var newState = $.extend({}, oldState, { title: newTitle });
// send the update
widgetState.onNext(newState);
});
// listen to new state values, probably to update your HTML?
widgetState.subscribe(function (newState) { ... });
// listen only when wasClicked is true
widgetState
.filter(function (s) { return s.wasClicked; })
.subscribe(function (s) { ... });

Related

How to process two sets from different models in one custom control

Aim:
I'd like to have two models(sets of data) passed to the custom control with a predefined search field, in which later on I can execute filtering.
I'm a newbie in OpenUi5, so I might be doing something wrong and stupid here. I've started with a simplified task of passing data from the frontend to my custom control and experiencing troubles.
Background of the simplified idea:
Create a custom control with an aggregation foo , the value to it will be provided from the view.
Also create another aggregation element _searchField which will be populated with the data provided from the view.
Fire the onSuggestTerm everytime user types in a _searchField.
Custom control code:
function (Control) {
var DropDownListInput = Control.extend('xx.control.DropDownListInput', {
metadata: {
defaultAggregation: 'foo',
aggregations: {
foo: { type: 'sap.m.SuggestionItem', multiple: true, singularName: 'suggestionItem' },
_searchField: { type: 'sap.m.SearchField', multiple: false, visibility: 'hidden' }
}
}
});
DropDownListInput.prototype.init = function () {
var that = this;
this.onSuggestTerm = function (event) {
var oSource = event.getSource();
var oBinding = that.getAggregation('foo');
oBinding.filter(new sap.ui.model.Filter({
filters: new sap.ui.model.Filter('DISEASE_TERM', sap.ui.model.FilterOperator.Contains, ' Other')
}));
oBinding.attachEventOnce('dataReceived', function () {
oSource.suggest();
});
};
this.setAggregation('_searchField', new sap.m.SearchField({
id: 'UNIQUEID1',
enableSuggestions: true,
suggestionItems: that.getAggregation('foo'),
suggest: that.onSuggestTerm
}));
};
return DropDownListInput;
}, /* bExport= */true);
I'm not providing Renderer function for control here, but it exists and this is the most important excerpt from it:
oRM.write('<div');
oRM.writeControlData(oControl);
oRM.write('>');
oRM.renderControl(oControl.getAggregation('_searchField'));
oRM.write('</div>');
Passing the data to this control from the xml frontend:
<xx:DropDownListInput
id="diseaseTermUNIQUE"
foo='{path: db2>/RC_DISEASE_TERM/}'>
<foo>
<SuggestionItem text="{db2>DISEASE_TERM}"
key="{db2>DISEASE_TERM}" />
</foo>
</xx:DropDownListInput>
The code fails to run with this error Cannot route to target: [object Object] -
and I have no idea what's wrong here..
The problem is that you forgot to provide single quotes in your path:
foo="{path: 'db2>/RC_DISEASE_TERM/'}"

how to determine if my ractive computed value has changed

For my Layout I have a component which needs to be initialized once rendering is completed, and then again if anything in my data changes.
This would work great, but I rely on computed values for a filtered and changed output for each client and via observe the change event is fired to often. What I do:
let myRactive = new Ractive({
el: '#clientContainer',
template: myTemplate,
magic: true,
modifyArrays: true,
data: {data}, //=> this is an awful lot of Data changing all the Time
computed: {
usefulData(){
let tempdata = this.get('data');
//=> a lot of rearranging, filtering and sorting
// to adapt the data only for this specific client
return tempdata;
}
onrender: function () {
initmyLayoutComponent()
}
});
So I tried to get it this way
myRactive .observe( 'usefulData', function ( newValue, oldValue, keypath)
destroymyLayoutComponent();
initmyLayoutComponent();
});
But this fired a) every time anything in datachanges (even when it was something completely unrelated to usefulData), and b) before ractive has rendered the DOM so the component gets re-initialized to early.
Is there a way to observe only the computed value, or - which would be even better - just observe specific actions in the computed value (like I want to react to added/deleted Objects, but not to changed Values)?
Well what you can do, is to actually send in a clientData obj into the template instead, and then only listen to that data.
let myRactive = new Ractive({
el: '#clientContainer',
template: '<div>{{clientData.name}}</div><input type="text" value="{{clientData.name}}" /><div>{{email}}</div><input type="text" value="{{email}}" /><div>Clientdata changed: {{cnt}}</div>',
magic: true,
modifyArrays: true,
data: {
name: 'hallo',
email: 'a#a.com',
cnt: 0
}, //=> this is an awful lot of Data changing all the Time
computed: {
usefulData() {
let tempdata = this.get('name');
// Create your own data obj
tempdata = {
name: 'world'
};
// set it to the ractive context
this.set('clientData', tempdata);
}
},
oninit: function() {
this.observe('clientData', function(newValue, oldValue, keypath) {
let cnt = this.get('cnt')
cnt += 1;
this.set('cnt', cnt);
console.log('listen only to the computed data');
}, {init: false});
this.get('usefulData');
},
onrender: function() {
// do something
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/ractive/0.9.0-build-123/ractive.min.js"></script>
<div id="clientContainer"></div>

Add a new Key: value pair into every existing array of a object JS, React.js

I'm new to react and learning it at a get go paste. I'm building a small PokemonApp that gets its data through the PokemonAPI and it render's it to the web.
The only data that I'm fetching through the API is the Pokemon name and URL of the Pokemon. The component that fetches the data is called getDataFromAPI and it uses axios (promises) to get it. The data is stored in the pokemoni array and is displayed on the browser. The state shown through the Crome's React Dew Tools is this.
State
pokemoni: Array[20]
0: {...}
name: "metapod"
url: "http://pokeapi.co/api/v2/pokemon/11/"
1: {...}
Now i wont to add a new [{key: value}] pair to all the existing objects that will change from false to true when the user clicks on a Pokemon that they got (it will be a button or something else). The state of the change by the user will be made only once (from false to true).
State
pokemoni: Array[20]
0: {...}
name: "metapod"
url: "http://pokeapi.co/api/v2/pokemon/11/"
havePokemon: "false"
1: {...}
The logic i came to was using .map() to add a new [{key: value}] pair to all the existing objects. And doesn't work. Because i don't understand the problem. The function is located inside the getDataFromAPI component and it is called updatePokemon
var PokedexApp = React.createClass({
componentDidMount: function(){
this.getDataFromAPI();
},
getDataFromAPI: function(){
var that = this;
PokemonAPI.getPokemonList().then(function (temp) {
that.setState({
pokemoni: temp,
isLoading: false
});
}, function (e) {
that.setState({
isLoading: true
});
});
//This function should update the array with a new key: value pair with the name havePokemon
var updatePokemon = this.state.pokemoni.map(function(myPokemon){
myPokemon.havePokemon= "false";
return myPokemon;
});
this.setState({pokemoni: updatePokemon});
},
componentWillUnmount: function() {
this.getDataFromAPI.abort();
},
getInitialState: function() {
return{
showMyPokemon: false,
searchPokemon: '',
pokemoni: [],
isLoading: false
};
},
render: function(){
var {pokemoni, showMyPokemon, searchPokemon} = this.state;
var filteredPokemoni = PokemonLocalStorageAPI.filterPokemone(pokemoni, showMyPokemon, searchPokemon);
return (
<div>
<h1 className="text-center page-title">PokedexApp</h1>
<PokedexAppSearch onSearch={this.handleSearch} />
<PokedexAppLista pokemoni={filteredPokemoni} onToggle={this.handleToggle}/>
</div>
)
}
});
module.exports = PokedexApp;
It does not work and i don't understand why. Ty in advance if u have the time to check my problem.

Firebase React Binding

I'm somewhat new to React, and using the re-base library to work with Firebase.
I'm currently trying to render a table, but because of the way my data is structured in firebase, I need to get a list of keys from two locations- the first one being a list of user keys that are a member of a team, and the second being the full user information.
The team node is structured like this: /teams/team_id/userkeys, and the user info is stored like this: /Users/userkey/{email, name, etc.}
My table consists of two react components: a table component and a row component.
My table component has props teamid passed to it, and I'm using re-base's bindToState functionality to get the associated user keys in componentWillMount(). Then, I use bindToState again to get the full user node, like so:
componentWillMount() {
this.ref = base.bindToState(`/teams/${this.props.data}/members`, {
context: this,
state: 'members',
asArray: true,
then() {
this.secondref = base.bindToState('/Users', {
context: this,
state: 'users',
asArray: true,
then() {
let membersKeys = this.state.members.map(function(item) {
return item.key;
});
let usersKeys = this.state.members.map(function(item) {
return item.key;
});
let onlyCorrectMembersKeys = intersection(membersKeys, usersKeys);
this.setState({
loading: false
});
}
});
}
});
}
As you can see, I create membersKeys and usersKeys and then use underscore.js's intersection function to get all the member keys that are in my users node (note: I do this because there are some cases where a user will be a member of a team, but not be under /Users).
The part I'm struggling with is adding an additional rebase call to create the full members array (ie. the user data from /Users for the keys in onlyCorrectMembersKeys.
Edit: I've tried
let allKeys = [];
onlyCorrectMembersKeys.forEach(function(element) {
base.fetch(`/Users/${element}`, {
asArray: true,
then(data) {
allKeys.prototype.concat(data);
}
});
});
But I'm receiving the error Error: REBASE: The options argument must contain a context property of type object. Instead, got undefined
I'm assuming that's because onlyCorrectMembersKeys hasn't been fully computed yet, but I'm struggling with how to figure out the best way to solve this..
For anyone dealing with this issue as well, I seemed to have found (somewhat) of a solution:
onlyCorrectMembersKeys.map(function(item) {
base.fetch(`/Users/${item}`, {
context: this,
asObject: true,
then(data) {
if (data) {
allKeyss.push({item,data});
this.setState({allKeys: allKeyss});
}
this.setState({loading: false});
},
onFailure(err) {
console.log(err);
this.setState({loading: false});
}
})
}, this);
}
This works fine, but when users and members state is updated, it doesn't update the allkeys state. I'm sure this is just due to my level of react knowledge, so when I figure that out I'll post the solution.
Edit: using listenTo instead of bindToState is the correct approach as bindToState's callback is only fired once.

How to listen on a single child change in react native firebase?

I have below code use once('value') to retrieve user's contact list by index. Then if someone change description, my contact list will not update in real time because it is once(). I can't use on() because this will cause crazy query leak. So where should I place on('child_added') to listen on every single node.
getContacts(userID) {
var contactList = [];
this.ref.child('userContacts').child(userID).once('value', (contacts) => {
contacts.forEach((contact) => {
this.ref.child('users').child(contact.key()).once('value', (contact) => {
contactList.push({
key: contact.key(),
name: contact.val().name,
description: contact.val().description,
numberOfProduct: contact.val().numberOfProduct
});
this.setState({
dataSource: this.state.dataSource.cloneWithRows(contactList)
});
})
})
})
}
If you want to receive updates for an object, you will have to attach a listener with on().
If you want to prevent attaching multiple listeners to the same user, keep an object where you keep the listener for each user-key you attached. Then before attaching a new user-listener, check if it already exists:
var userListenersByKey = {};
this.ref.child('userContacts').child(userID).once('value', (contacts) => {
contacts.forEach((contact) => {
if (!userListenersByKey[contact.key()]) {
userListenersByKey[contact.key()] = this.ref.child('users').child(contact.key()).on('value', (contact) => {
contactList.push({
key: contact.key(),
name: contact.val().name,
description: contact.val().description,
numberOfProduct: contact.val().numberOfProduct
});
this.setState({
dataSource: this.state.dataSource.cloneWithRows(contactList)
});
});
}
});
})
You'll also want to remove these listeners at some point, so it might be better to listen for child_added and child_removed for userContacts and use that to (optionally) add and remove the user-listeners.

Categories