Observing multiple observables + .map()

I have a regular type view of a list of items (array of objects) that the user can filter by typing text into a TextBox, but I also want to be able to sort it, so I have some buttons for selecting order by fields (e.g. name, count)

I can do either one by e.g. (and none of this is actual code, just written from memory here):

var myItems = [
  { name: "foo", count: 14 }, {}, {}, {}
]

var textFilterObservable = Observable("")
var orderSelectionObservable = Observable("id");

var exportedList = textFilterObservable.map(function(t) {
  var re = new RegExp(t);
  return myItems.filter(function(i) {
    return re.test(i.name);
  })
}).inner()

OR I can do the ordering

var exportedList = orderSelectionObservable(function(order) {
  return myItems.sort(function(a,b) {
    return a.count - b.count;
  })
}).inner()

BUT how on Earth am I meant to combine these two in a sensible way?

I’ve tried making a “compound observable”, like this

var both = Observable({ text: textFilterObservable, order: orderSelectionObservable }) // even .inner()
var exportedList = both.map(function(b) { ..never fires on UI changes })

I’ve tried both.addSubscriber() also…

the both.map() seems to fire if I set something in code, but not through UI changes/clicks…

How are you meant to do this in a clean way? (I’ve done ugly hacks…)

Here’s a quick example hope this helps :slight_smile: let me know if you don’t understand something

<App>
	<JavaScript>
		var Observable = require('FuseJS/Observable');
		var items = Observable();
		var sortProperties = Observable('id', 'name', 'count');
		var propsLength = sortProperties.toArray().length;
		var searchText = Observable('');
		var data = [
			{
				name: 'Rune',
				count: 10,
				id: 0
			},
			{
				name: 'Edwin',
				count: 5,
				id: 1
			}
		]

		function sort(evt) {
			var sortedItems = items.toArray().sort(function (itemA, itemB) {
				return itemA[evt.data].toString().localeCompare(itemB[evt.data].toString());
			});
			items.refreshAll(sortedItems);
		}

		function filter(input) {
			var props = sortProperties.toArray();
			var val = input.value.toLowerCase();
			if(val !== '') {
				var filteredItems = items.toArray().filter(function (item) {
					for(var prop of props) {
						if(item[prop].toString().toLowerCase().indexOf(val) > -1) return item;
					}
				});
				items.refreshAll(filteredItems);
			} else {
				items.refreshAll(data);
			}
		}

		function loadData() {
			// fetch your data here (Using the data object for example)
			items.refreshAll(data);
		}

		loadData();

		module.exports = {
			searchText: searchText,
			filter: filter,
			items: items,
			sortProperties: sortProperties,
			propsLength: propsLength,
			sort: sort
		}
	</JavaScript>
	<DockPanel>
		<Basic.TextInput Dock="Top" Value="{searchText}" PlaceholderText="Search Here" PlaceholderColor="#555" TextAlignment="Center" ValueChanged="{filter}" />
		<StackPanel Dock="Top" Orientation="Horizontal">
			<Each Items="{sortProperties}">
				<Basic.Button Text="{}" Clicked="{sort}" />
			</Each>
		</StackPanel>
		<ScrollView>
			<Grid ColumnCount="{propsLength}">
				<Each Items="{sortProperties}">
					<Text Value="{}" Color="Black" />
				</Each>
				<Each Items="{items}">
					<Text Value="{id}" Margin="5" />
					<Text Value="{name}" Margin="5" />
					<Text Value="{count}" Margin="5" />
				</Each>
			</Grid>
		</ScrollView>
	</DockPanel>
</App>

Hey Rune,

your use-case is somewhat complicated and requires to do a little ritual dance around the fire to get it working. Knowing some magic spells and yelling them out at the midnight, while running naked around your house in circles might help too.

Anyway, here’s the thing. As you have correctly found, you have to somehow bind to both search and filter, and you’ve already tried to do that. The approach you took failed because of one simple reason - one of those has to be the primary criteria. I imagine you’d first like to sort the list, and then filter it by the search keyword.

So I took the time and liberty to write a fully functional snippet that does exactly what you needed:

<App Background="#eee">
	<JavaScript>
		var Observable = require('FuseJS/Observable');

		var data = Observable();
		var search = Observable('b');
		var order = Observable('title');
		
		function SingleEntry(id, title) {
			this.id = id;
			this.title = title;
		};

		function fillData() {
			data.add(new SingleEntry(1,'EA title'));
			data.add(new SingleEntry(2,'DE title'));
			data.add(new SingleEntry(3,'CD title'));
			data.add(new SingleEntry(4,'BC title'));
			data.add(new SingleEntry(5,'AB title'));
		};

		// call fillData here so that there is something in data observable before the reactives fire
		fillData();

		var ordered = order.map(function(orderKey) { // this here fires whenever the order value changes
			var tmp = data.toArray(); // now we tap into the data variable
			tmp.sort(function(a,b) {
				if (a[orderKey] > b[orderKey]) {
					return 1;
				}
				if (a[orderKey] < b[orderKey]) {
					return -1;
				}
				return 0;
			});
			return tmp;
		}).inner(); // get the inner array, so we do not have an array in an array

		var searched = search.map(function(keyword) { // this here fires whenever the search value changes
			var pattern = new RegExp(keyword, 'i');
			return ordered.where(function(e) { // notice how we tap into ordered, which is reactive
				return pattern.test(e.title);
			});
		}).inner(); // and again, get the inner thing

		searched.addSubscriber(function(x) {
			console.log('searched and ordered: ' + JSON.stringify(x));
		});

		module.exports = {
			'searched': searched // make sure you only export the last thing in the reactives row, searched
		};
	</JavaScript>

</App>

Sorry for not adding any UX, but the addSubscriber makes it fire anyway and logs stuff to Monitor. I’m sure you can get it going from here.

Aha, I added some quick UI to it and I see it works nicely. Thanks!

It was the // notice how we tap into ordered, which is reactive .where() part with the chaining that was the part where I couldn’t figure out how to chain the RX bits.

I will try to rewrite my case using similar code :slight_smile: