Angular subscribe push multiple object to array - arrays

i'm making an angular application and i have an empty array
addresses : any [] = [];
then after a click event i start an http observable to charge the data from a server
and then i make a pipe map to take what i want and then i make a subscribe
const URL ='some url';
onSearch() {
this.httpClient
.get(`URL${{some data to generate the http observable}}`)
.pipe(
map(response => response.address),
tap( => console.log("address array", address)) // address array [Object, Object, Object]
)
.subscribe(address => this.addresses = address);
}
in the html template i have this
<button (click)="onSearch()"> search address </button>
<ul *ngFor="let address of addresses">
<li>{{address.one}} - {{address.two}} - {{address.three}}</li>
</ul>
the firs time i clcik the button is ok i see the result , but the second time i click the button
the first result is deleted from the second.
My problem is how can i store multiple result in an array and how can i see thei in the html temlate?

Leveraging angular's change detection
You can try this.
.subscribe(addressArray =>
addressArray.foreach(
address => this.addresses.push(address)
)
);
or this:
.subscribe(addressArray => {
for (const address of addressArray){
address => this.addresses.push(address);
}
});
These two approaches do not work well with angular change detection since they're are mutating an existing array rather than creating a new array.
These approaches both create new arrays and angular change detection will have an easy time spotting.
this:
.subscribe(addressArray =>
this.addresses = this.addresses.concat(addressArray)
);
or this:
.subscribe(addressArray =>
this.addresses = [...this.addresses, ...addressArray]
);
They're all very similar ways of achieving the same thing.
Leveraging Angular's async Pipe
Here's a very different way: Output your result array directly in your stream!
Instead of an array like this:
addresses: any[] = [];
You have an Observable and a Subject like this:
newAddresses = new Subject<any[]>();
addresses = newAddresses.pipe(
scan((acc, val) => ([...acc,...val]), [])
)
Then your search emits results directly to your subject.
const URL ='some url';
onSearch() {
this.httpClient
.get(`URL${{some data to generate the http observable}}`)
.subscribe(response => this.newAddresses.next(response.address));
}
Now in your HTML, you can subscribe to this array using angular's async pipe
<button (click)="onSearch()"> search address </button>
<ul *ngFor="let address of addresses | async">
<li>{{address.one}} - {{address.two}} - {{address.three}}</li>
</ul>
This is a reasonable amount of extra work. So why bother?
Flexibility: Now any number of methods can update your addresses, and they'll all get managed by scan in a uniform way.
Maintainability: If you want to change or format addresses in the future, there's a simple & singular place to do it.
Extenability: You can add in fancy RxJS operators like debounce to control how results appear on the screen. It's a really nice way to stop display-stuttering when events get queued up.
Performance: If your application relies on pushing data rather than binding it, you can turn off angular's change detection entirely and boost the performance of your app :).
Even if you're not going to turn off change detection, you can mutate a massive array in memory and not worry about whether change detection will pick it up or need to explicitly tell change detection to check the contents of your array for changes.

You can try this.
.subscribe(address => this.addresses = [...address, ...this.addresses]);

Related

Mutating array within an array (Polymer iron-list)

I currently have an iron-list within another iron-list. The parent's data comes from a firebase-query element, and the child's data is computed from each parent item. The db structure and code looks a bit like this:
DB: [
category1: [
itemId1: {
price: 10,
title: "title"
}
]
]
<iron-list id="categoryList" items="{{categories}}" multi-selection as="category">
<template>
<div class="category-holder">
<iron-list id="{{category.$key}}" items="{{_removeExtraIndex(category)}}" as="item" selection-enabled multi-selection selected-items="{{selectedItems}}" grid>
<template>
<div class$="{{_computeItemClass(selected)}}">
<p>[[item.title]]</p>
<p>[[item.price]]</p>
</div>
</template>
</iron-list>
</div>
</template>
</iron-list>
After selecting any number of items, the user can tap on a fab to batch edit the price. This is where I'm having issues. I can't figure out how to access the correct child iron-list in order to call list.set...I'm currently trying the following very nasty method:
var categories = this.$.categoryList;
var categoryItems = categories.items;
(this.selectedItems).forEach(function(item) {
var index = item.itemId;
categoryItems.forEach(function(itemList, categoryIndex) {
if (itemList[index]) {
categories.set('item.' + categoryIndex + '.price', 10);
}
}, this);
}, this);
I'm iterating over the selected items in order to extract the item index and then iterating over the parent iron-list data (categoryItems) in order to check if the given item exists in that subset of data. If so, then I use the category index and attempt to call set on the parent iron-list using the given path to access the actual item I want to edit. As expected, this fails. Hopefully I've made myself clear enough, any help would be appreciated!
EDIT #1:
After much experimenting, I finally figured out how to correctly mutate the child iron-list:
(this.selectedItems).forEach(function(item) {
var list = this.$.categoryList.querySelector('#' + item.category);
var index = list.items.indexOf(item);
list.set(["items", index, "price"], 30);
}, this);
A couple of things worth noting. I'm using querySelector instead of the recommended this.$$(selector) because I keep running into a "function DNE" error. But now I have another problem...after calling the function, the value gets updated correctly but I get the following error:
Uncaught TypeError: inst.dispatchEvent is not a function
Here's a picture of the full error message:
I see the light, hopefully someone can help me out!
OK, I'll take a shot at this. I think the following happens, and I guess this based on how dom-repeat works:
var categories = this.$.categoryList;
var categoryItems = categories.items;
You take the variable that the iron-list is based on, but setting one array to another just creates a reference in javascript. As soon as you update categoryItems, you also update this.$.categoryList.items. When you later sets the new value, iron-list will do a dirty check and compare all subproperties, and because they are equal (because ... reference), the iron-list wont update the dom.
What you should do is to make sure it's a totally new copy and the way of doing that is to use JSON.parse(JSON.stringify(myArray)).
Further on, one major flaw I see in your code is that you're using querySelector to select an element, and then manipulate that. What you should do is to use this.categories and only that variable.
So your method should look something like:
// Get a freshly new array to manipulate
var category = JSON.parse(JSON.stringify(this.categories);
// Loop through it
category.forEach(category) {
// update your categoryList variable
}
// Update the iron list by notifying Polymer that categories has changed.
this.set('categories', category);

Display content of an Array of Objects with Interpolation in Angular2 Typescript

Application
A simple Search bar and a button where user enters a keyword and the response returned is from a RESTful server (HTTP GET requests)
simplesearch.ts
export class SimpleSearch {
kw: string; // keyword
resp: string; // response from Server
}
simplesearch.service.ts
Has a simple method called searchData which does a HTTP GET request with the user's keyword as a query search. (Code not included for brevity)
simplesearch.component.ts
/*All respective headers and #Component removed from brevity*/
const OUTPUT: SimpleSearch[] = []; // create an array for storing Objects
export class SimpleSearchComponent {
Output = OUTPUT; // define variable for array
constructor(private httpServ: SimpleSearchService, private temp: SimpleSearch) {}
/*Search button on HTML file does this*/
Search(inputVal: string) {
this.temp.kw = inputVal; // store the value of user's input
this.httpServ.searchData(inputVal)
.then(res => this.temp.resp = res); // store the response in temp.resp
// push the Object on the Output Array
this.Output.push({kw: this.temp.kw, resp: this.temp.resp});
}
}
Interpolation Variable
I use Output as an Interpolation Variable for my HTML template. I show the data in an unordered list
<ul>
<li *ngFor="let keyword of Output">
<span>{{keyword.kw}}</span>
</li>
</ul>
Response:
<ul>
<li *ngFor="let answer of Output">
<span>{{answer.resp}}</span> <!-- WHAT TO DO HERE for Array Index-->
</li>
</ul>
Result
I can see the keywords in a list every time a user inputs new keywords but
the responses in the wrong way
How do I pass Indexing with the Interpolation? Or am I thinking wrong?
The easy way out was to create two separate Array<String> for keywords and responses and this works great since I can use the index to delete the contents on the page too but with an Object in an Array I am confused with the key: value representation and the index of the Array (OUTPUT) itself.
The problem lies exactly where developer noticed, this.temp.resp is outside the async function. So when you are pushing items in your Output array, it's always pushing the previous search with the new keyword, therefore you are getting the behavior that the resp is always "one step behind". You can check this to understand this async behavior: How do I return the response from an Observable/http/async call in angular2?
So let's look at the code and explain what is happening. I assume you have initialized 'temp' since it isn't throwing an error on first search, where temp.resp would be undefined unless temp is initialized.
this.httpServ.searchData(inputVal)
// this takes some time to execute, so the code below this is executed before 'this.temp.resp' has received a (new) value.
.then(res => this.temp.resp = res);
// old value of 'temp.resp' will be pushed, or if it's a first search, empty value will be pushed
this.Output.push({kw: this.temp.kw, resp: this.temp.resp});
So how to solve this, would be to move the this.Output.push(... line inside the callback (then), so that the correct values will be pushed to the array.
Also I'd change your model to be an Interface instead of Class. But as to how to change the function and do the assignment inside the callback, I'd also shorten the code a bit and do:
Search(inputVal: string) {
this.httpServ.searchData(inputVal)
.then(res => {
// we are pushing values inside callback now, so we have correct values!
// and 'SimpleSearch' stands for the interface
this.Output.push(<SimpleSearch>{kw: inputVal, resp: res});
});
}
}
This should take care of it that the corresponding keyword will have the corresponding response! :)
PS. Worth noticing here, is that you'd maybe want to display the keyword while we are waiting for the response for that keyword, I ignored it here though and applied the same as you are currently using.

How to process this json-response in RxJS?

I have angular2 app (TypeScript), which uses RxJS to process json-request (I know about angular's services, I have just simplified code).
This code is in class method:
this.http.post(url, body, options)
.map(
response => response.json()
)
.subscribe(result => {
persons => this.persons = persons
});
In class I have field persons and I want that this field should get data from response in order to use this persons in *ngFor
Also I have java-backend, which produces such response (I have got this output after adding console.log() in map()):
message:"[{"personId":"29d5a903-b664-4fc3-9bd9-0836f1b3dc58","email":"test#example.com","firstname":"Test","lastname":"Testoff","username":"oooo","birthdate":"2000-05-28","gender":"male","password":"xxxxx","active":null,"profiles":[]}]"
What am I doing wrong ? (I'm backender, sorry, for silly questions, may be)
I saw that you fixed the code and it is working. I'd like to outline a different solution which is more aligned with angular2's philosophy, do not hold a local variable in your app, use directly the service's sequence:
<my-person
*ngFor="let person; of peopleService.people | async;"
[name]="person.name"
(hello)="saidHello($event)">
</my-person>

Ordering an observable collection with Reactiveui

I am having some difficulty with ordering an observable collection in my ViewModel.
Here is my situation:
In my view model, I have the following list:
public List<TicketModel> Tickets
{
get { return _Tickets.Value; }
set
{
{
this.RaiseAndSetIfChanged(c => c.Tickets, value);
}
}
}
private ObservableAsPropertyHelper<List<TicketModel>> _Tickets;
This list is populated using a ReactiveAsyncCommand:
LoadTickets.RegisterAsyncFunction(x => loadTickets())
.ToProperty(this, x => x.Tickets);
All works so far.
I have another command, SortByCommand which gets called whenever the user wants to sort a collection. The command looks like this:
SortByCommand = new ReactiveCommand(this.WhenAny(c => c.Tickets, ((tickets) => tickets.Value != null && tickets.Value.Count > 0)));
SortByCommand.Subscribe(c => sortTickets((SortByModel)c));
The command also calls a function that orders the collection using an order by clause:
private void sortTickets(SortByModel model)
{
Tickets = Tickets.OrderBy(model.Selector).ToList();
}
Whenever the sortTickets function is called, there is an exception thrown which says:
Unable to cast object of type 'ReactiveUI.ObservableAsPropertyHelper`1[System.Collections.Generic.List`1[Bugmine.Modules.MyPage.Models.TicketModel]]' to type 'System.Collections.Generic.List`1[Bugmine.Modules.MyPage.Models.TicketModel]'.
I have several questions:
1) Why can't I set directly the Tickets model? Do I need to first convert the result of the OrderBy to some sort of observable collection?
2) Is there a better way of doing this?
EDIT: Clarification
The approach I am taking right now is:
The Tickets collection gets reset every x seconds.
As soon as the sortTickets function is called, I will sort and reset this collection by:
Tickets = Tickets.OrderBy(c => c.Name).ToList(); //for example
When the Tickets collection is loaded again, I will check if it should be sorted and sort it before setting the Tickets property.
This feels a bit hacky because I am basically setting the collection at two points - upon loading and upon sorting. Moreover, upon loading I am using the ReactiveUI helper -> ToProperty:
LoadTickets.RegisterAsyncFunction(x => loadTickets())
.ToProperty(this, x => x.Tickets);
Whereas, upon sorting I am doing that myself:
Tickets = Tickets.OrderBy(model.Selector).ToList();
I am wondering if there is a better way to do the sorting using the ReactiveUI approach which I already use upon loading.
Thanks in advance!
Another way to solve this is via CreateDerivedCollection:
SortedTickets = Tickets.CreateDerivedCollection(
x => new TicketViewModel(x),
orderer: (l,r) => SortModel.Selector(l, r), // Returns CompareTo() result
signalReset: this.WhenAny(x => x.SortModel, x => x.Value)); // Reorder on SortModel change
Note that this breaks down if Tickets is set repeatedly (which in this case it is) - you might change your model to initializing Tickets in the ctor, then Clearing and Adding all the items, i.e.
LoadTickets.RegisterAsyncFunction(x => loadTickets())
.Subscribe(x => {
// TODO: Make sure Tickets is a ReactiveCollection
Tickets.Clear();
Tickets.AddRange(x); // Will trigger resorting of SortedTickets
});
Just had a look on Ana's blog http://blog.paulbetts.org/index.php/2010/07/05/reactivexaml-series-implementing-search-with-observableaspropertyhelper/
//
// This is the canonical way to make a read-only property whose value
// is backed by an IObservable
//
ObservableAsPropertyHelper<List<FlickrPhoto>> _Photos;
I think the read-only is the important point there.
Instead you could try using a normal observable where you can use OnNext to push in the new value
private Observable<List<TicketModel>> _Tickets = new Observable<Lis<TicketModel>>();
_Tickets.OnNext(newValue);
Or use a ObservableForProperty<> and just use the property normally
public List<TicketModel> _Tickets { get;set;}
private Observable<List<TicketModel>> _ticketsObservable= ObservableForProperty<..>(x=>x.Tickets);
Both of these methods expose an Observable which we can use later in the sort.
Why not try handling the two inputs into your sort in the same way, then it wont feel so hacky. That way you'll also have an
public SortModel SortModel {get;set;}
Your sort command implementation becomes
SortByCommand.Subscribe(c => _Sort = c));
but then you subsribe to both ticket changes AND sort criteria changes in one, see http://rxwiki.wikidot.com/101samples#toc44 for CombineLatest
new ObservableForProperty<..>(x=>x.SortModel)
.CombineLatest(_ticketsObservable)
.Subscribe( (x,y)=>
{
//Refactor to SortMethod
_tickets = y.OrderBy(x.Selector);
});
I'm pretty sure that the result of OrderBy(..) is an IEnumerable instead of an ObservableCollection. Fortunately though, it has a constructor that can do the conversion, i.e.
Tickets = new ObservableCollection<...>(Tickets.OrderBy(...));
"Better" can be very subjective measure. Firstly you haven't explained the full lifespan of the Tickets collection and the tickets it contains, so we can't really tell.
You might consider using a SortedTickets field/property instead of overwriting your tickets property which you might find you wouldn't need to be an observable collection as you would know when it needs to NotfyProperryChanged from the Tickets observable. It all depends on how frequently your tickets is likely to change.
P.S. Also make sure you have tested what happens to your sorted list when you add another ticket as you can't just

How do I send arrays between views in CakePHP

I am not sure if I formulated the question right, but still ...
I have a view that shows a flash embed and this flash take as parameter a /controller/action URL that generates a XML. I nee to send, from this view, an array to the XML generator action. How is the best way ? Is there some helper->set() method like or I have to create an specific URL to send this array to that action ?
Here goes my structure:
my_controller.php
function player() {}
player.ctp
<div id="myDiv">Here it Goes</div>
<script type="text/javascript">
var so = new SWFObject('player.swf','test','50','50','8');
so.addVariable('file','/xml/generate'); // need to pass an array here
so.write('myDiv');
</script>
xml_controller.php
public function generate() {
// I need to read an array here
}
generate.ctp
echo "<xml><data>" . $array['contents'] . "</data>";
If the array is small enough, serialize then urlencode it and add it as a paramter to the url to your generate action:
player.ctp
so.addVariable('file','/xml/generate/<?php echo urlencode(serialize($array)); ?>');
then read it back:
public function generate($array) {
$array = unserialize($array);
}
Save the array in the session then in the next request to the XML generator action, read it back from the session.
my_controller.php
function player() {
$this->Session->write('key', $array);
}
xml_controller.php
public function generate() {
$array = $this->Session->read('key');
}
However, I have heard of some problems where flash sometimes doesn't send session cookies, in which case, append the session id to the url of the action:
so.addVariable('file','/xml/generate/<?php echo $session->id(); ?>');
and to get the session back:
public function generate($sessionId) {
CakeSession::id($sessionId);
$array = $this->Session->read('key');
}
First of all you cannot send data from one view to another in the manner you are speaking. Each of those calls would be a separate request and this means that it goes out of the framework and then in again. This means that the framework will be built and tear down between calls, making impossible to pass the data between views.
Now in regards to the array that has to be sent to your action, I'm utterly confused. I don't think you are looking at the problem the right way. If that action needs an array of data and then produce XML so the Flash Object can get it, then it makes even less sense. Are you sure that the Flash Object isn't the one responsible to sending that array of data to the Param you mentioned?
Well, even if all you are saying has to be done quite like that, I'll suggest you drop that array on the file system and then pick it up when the action is called by the Flash.
Or another suggestion would be to use AJAX to send that array to the action.
Both suggestions imply my utter "cluelessness" on your predicate.
I still have to ask, isn't the Flash Object gonna do something in all this?
You can send an array with data from a view to a controller in CakePHP like this.
To the link you can pass arguments:
www.site.com/model/action/param1:foo/param2:test
You can then retrieve them in the controller action in the following way:
$yourarray = $this->params['named'];
Of course the array shouldn't be too large then.

Resources