Angular2 SharedService passing ID as default but can be changed - javascript

I have created a sharedService() to get all the items in a particular location, I have put a default ID value of 3 but I want to have a dropdown menu where they can change a location Id: 4, Id: 5.
private sourceSiteId = 3;
getOriginSite() {
return this.http.get('http://www.example.com/api/Orders?sourceSite=' + this.sourceSiteId)
.map(res =>res.json());
}
then I call this sharedService in my component to get the items and output it using *ngFor, when a user change location it should also update the list of items base on the chosen location.
Edit: I put the wrong function, it should be this one which is also being called in ngOnInit()
getOrdersFromOrigin() {
this.sharedService.getOriginSite()
.subscribe(data => {
this.items= data.Results
}
item lists ngFor which needs to be updated when selecting a location. This is the order list html which has the nav as
<nav></nav>
<div *ngFor="let x of items">
<button>{{ x.name }}</button>
</div>
At the moment I've only created a dummy locations which to get the logic. This is from the nav component which is a child component in the order list html.
this.sourceSites = [
{
'Id': 1,
'StoreLocation': 'Eastleigh'
},
{
'Id': 2,
'StoreLocation': 'Portchester'
},
{
'Id': 3,
'StoreLocation': 'Soho'
},
{
'Id': 4,
'StoreLocation': 'Basingstoke'
}
]
<ul class="dropdown-menu">
<li *ngFor="let store of sourceSites">{{ store.StoreLocation}}</li>
</ul>
Update
This is what I have, I pass the location ID in my click function which then I pass to the service. But this doesn't have the default value of 3 because getOrderById() is being triggered in ngOnInit, so the initial API call will then be null
This is in my ChildComponent
changeSite(sourceSite) {
this.sharedService.getOriginSite(sourceSite)
.subscribe(data => {
console.log(data); // gives me the data I want to render in the parent ngFor
})
}
SharedService
getOriginSite(sourceSite) {
return this.http.get('http://myexample.com/api/Orders?sourceSite='+ sourceSite)
.map(res =>res.json());
}
How can I change the sourceSite=3 value to a chosen location and update the list once location is chosen?

You have different solutions for your problem, depending on how you structured your app.
Lets say your dropdown is a child component and the parent is the one calling the service.
On your dropdown you create an Output() that emits an event when the value changes/user clicks.
Something like:
public idChanged: EventEmitter<number> = new EventEmitter();
//Click handler
public optionClicked(id: number): void {
this.idChanged.emit(id);
}
Then the parent component subscribes to that output like this:
<dropdown (idChanged)="changeId($event)"></dropdown>
And on the parent class we create the changeId() method and call the service to update the list with the new ID.
public changeId(id: number): void {
//Call service with the new Id.
}
Another solution would be storing the ID in the service, and every time the ID changes in the dropdown, update the service directly with the ID.

Related

Vue on changing array object trigger get method to check for changes

I have a button component that looks like this
<template>
<button
class="o-chip border-radius"
:class="{
'background-color-blue': theValue.isSelected,
'background-color-light-grey': !theValue.isSelected,
}"
#click="onSelection(theValue)"
>
{{ theValue.displayText }}
</button>
</template>
And when it is pressed it sets it isSelected state and emit an event to the parent component
private onSelection() {
this.theValue.isSelected = !this.theValue.isSelected;
this.$emit('selected', this.theValue);
}
So far so good the issue is in the parent component. Here I go through all the items that is the array that creates the button components above. The .value property is the unique identifier here so what I do is I look through the array with the item sent from the button component, then when found i use splice to trigger reactivity to replace that array object with the one sent from the button component. When i console.log this it works it shows that isSelected property now is true.
private selectedItem(item: Item) {
var index = this.itemSelectOptions
.map((p) => p.value)
.indexOf(item.value);
this.itemSelectOptions.splice(index, 1, item);
console.log(this.itemSelectOptions);
}
But then i have this get method that checks for anyChanges on this array and other things and then render UI based on true/false here. The issue is that when this array get changed the anyChanges method does not react and remains false.
private get anyChanges(): boolean {
console.log(this.itemSelectOptions);
return this.itemSelectOptions!.some((p) => p.isSelected);
}
How do i make it so that the anyChanges get method reacts on the changes made to itemSelectOptions which is also an get function
private get itemSelectOptions(): Item[] {
return this.items
? this.items.map((item) => ({
value: item.id.toString(),
displayText: item.displayName.toString(),
isSelected: false,
}))
: [];
}
What you want is a watcher on itemSelectOptions
watch: {
question(newOptions, oldOptions) {
this.anyChanges(newOptions)
}
},
guess it will look smthing like this ^^
https://vuejs.org/guide/essentials/watchers.html#basic-example
The reason this was not working was that since itemSelectOptions where a get function it should/can not be modified. So changing it into a private variable and initializing it solved it like this.
private itemSelectOptions: Item[] = [];
and then initializing
this.itemSelectOptions = this.items
? this.items.map((item) => ({
value: item.id.toString(),
displayText: item.displayName.toString(),
isSelected: false,
}))
: [];

Passing data into Angular 13 modal

I'm trying to pass data into a modal. The modal is a separate component alltogether.
I'm able to console.log the bsModalRef property in the constructor, but for some reason, I can't console.log the bsModalRef.content property.
Here is my code in the page I'm launching the modal from...
<button class="btn btn-primary"
type="button"
(click)="openDeleteModal(result.id)">
Edit
</button>
Here is the code for that openDeleteModal() method...
public openDeleteModal(id: number): void {
this.selectedCase = this.listOfSearchResults.filter(result => result.id == id);
const initialState: ModalOptions = {
initialState: {
list: [
this.selectedCase
],
title: 'Edit Case',
whatYouWantToDelete: 'this error'
}
};
this.bsModalRef = this.modalService.show(DeleteModalComponent, initialState);
}
Now, for the code in the modal itself, I'm importing BsModalRef like this...
import { BsModalRef } from 'ngx-bootstrap/modal';
Here are the properties I set...
title?: string;
whatYouWantToDelete?: string;
list?: any[];
initialState: any;
and this is what I have for the constructor...
constructor(
public bsModalRef: BsModalRef,
private http: HttpClient) {
this.list = this.list;
console.log("this.list: ", this.list);
console.log("this.bsModalRef in constructor: ", this.bsModalRef);
console.log("this.bsModalRef.content in constructor: ", this.bsModalRef.content);
}
this is the screenshot of what I'm seeing in the console.log...
And this is the content part of the BsModalRef object...
My question is, how do I access that data in the list property from the constructor? That object has properties that I need to populate a form I have in the modal.
Stated differently...
this line of code...
this.bsModalRef = this.modalService.show(DeleteModalComponent, initialState);
opens the modal and passes in the data as initialState. How do I access the data that I'm passing in through the initialState object from within the modal itself?
Here is a screenshot that shows I can see the data in the NgOnInit, but the issue is I can't get that data to show up in the modal html file.
Here is the html in the delete component file where I'm trying to display the list.
<p>Are you sure you want to delete {{ this.whatYouWantToDelete }}?</p>
<p>id selected: {{ this.products }}</p>
<div *ngFor="let user of this.list; index as i">
Case Id: {{user.caseId}}
</div>
There are 2 key notes here:
Objects logged in the Javascript console are "live", so expanding them shows the current contents of the object. Content.list was empty when you first called console.log(). Then data was updated and hence on console too. See issue
In order to see real data I prefer this:
console.log(JSON.parse(JSON.stringify(this.bsModalRef)));
Seems there is issue with bsModalRef. You can access content via:
setTimeout(() => { console.log(this.bsModalRef.content), 0});
But it is a workaound. You should report this to ngx-bootstrap repository.
My question is, how do I access that data in the list property from the constructor? That object has properties that I need to populate a form I have in the modal.
The proper way to access data is within ngOnInit lifecycle hook method .
title, whatYouWantToDelete and list data would be present within ngOnInit; you don't need to access it via bsModalRef instance.
Try this:
ngOnInit() {
console.log(this.list);
console.log(this.whatYouWantToDelete);
console.log(this.title);
}
Edit:
Since this.list is an array of array as per the screenshot, it means that user is an array within ngFor. Try user[0]?.caseId

How do I send data into an Angular component only once (since #Input listens for updates all the time)

I have an Angular Scroller component
<app-scroller></app-scroller>
that provides a skeleton for displaying an array of images
Random Component
<app-scroller [init]="getImages('cats')"></app-scroller>
<app-scroller [init]="getImages('dogs')"></app-scroller>
getImages(src: string) {
//THIS FUNCTION GETS CALLED AGAIN AND AGAIN
return {
aspect: '16/9',
res: 'min',
sources: this.imageService.getImagesFromAPI(src)
};
}
Scroller Component
public movies: string[] = [];
#Input() init: {aspect: string, res: string, sources: Promise<string[]>};
ngOnInit() {
this.init.sources.then(images => this.movies = movies);
}
but this results in the the getImages and therefore the sources Promise to be executed over and over
Is there a way I can send data to the Scroller component only once (therefore without using #Input() )
I believe you need to call your service once to get the array of images and save it inside your component as a property,
something like this
myService.getData.subscribe(data=> this.catImages = data)
If I understand your question and setup correctly, you are asking about preventing an #Input from listening, what if you instead prevent data from emitting to this input?
You could deliver an observable stream that emits just once, eg.
catImages$ = this.catDataFromService$.pipe(
take(1),
)
<app-scroller [source]="catImages$ | async"></app-scroller>
Alternatively, you could construct your own Observable and complete it when necessary.
Use property binding only to send the category id (dogs/cats) to the component and call getImages(cateogryID) only once in the child component.
Parent component
<app-scroller [categoryId]="catIdProperty"></app-scroller>
Child component:
#input()
categoryId: string;
images: [type here] = [initialization here];
ngOnInit(): void {
this.images = this.getImages(categoryId); // Btw, could getImages() reside in the imageService?
}

FormArray and other arrays of child Component (#ViewChildren) are empty

I have #ViewChildren child Component(BookFormComponent) inside a parent component(LibraryComponent). In my parent component I make a service call to get one BookData object.
I give the DookData object to a method of the child component initBookData(...). I want the child component to use the BookData to initialize its form controls. The BookData has an attribute selectedTypes which contains an array of books the user has already selected. I use the array to check its checkboxes.
There are 10 checkboxes and for instance if a user has 5 elements in the selectedTypes array then those 5 elements has to be checked out of the 10 checkboxes when the view is displayed.
The issue am having now is the form controls for name and color are initialized with the values from the BookData object but the checkboxes are not checked(selected) when the view is displayed. I did console.log()'s inside initSelectedTypes(....) of the child component and the lengths of the arrays are 0's meanwhile the child component uses the same arrays to display the checkboxes in the UI but when it has to use the same array to check(select) some of the checkboxes then the lengths are 0's.
My understanding is that the <book-form #book></book-form> in the parent component UI is the same as the attribute #ViewChildren(BookFormComponent) book: QueryList<BookFormComponent>; in the component class. So since the view is displayed then when I call a method on the attribute (book) then I expect all attributes of (book) to be initialized as well. I don't expect the arrays to be empty. All checkboxes are displayed correctly in the view but when I call initBookData(...) the arrays are empty.
I am using #ViewChildren because I tried #ViewChild and I was getting "undefined" (so could not even call the child's methods)
(I have omitted certain things in the code snippet to conserve space):
interface BookData {
name?: string,
color?: string,
selectedTypes?: Array<string> // this array contains the types a user has selected already
}
// PARENT COMPONENT CLASS
class LibraryComponent implements AfterViewInit, {
#ViewChildren(BookFormComponent) book: QueryList<BookFormComponent>;
// ADDITIONAL CHILDREN FOR OTHER mat-step omitted for clarity
bookData: BookData = {}
ngAfterViewInit(): void {
this.getBookData();
this.book.changes.subscribe((algemen: QueryList<BookFormComponent>) => {
book.first.initBookData(this.bookData);
});
}
// this method returns one book from the server and assigns it to "this.bookData"
getBookData() {
bookdataService.getBookData().subscribe(book => {
this.bookData = book;
});
}
}
// PARENT COMPONENT UI
<mat-horizontal-stepper #stepper linear>
<mat-step [stepControl]="book.bookForm">
<ng-template matStepLabel>Book</ng-template>
<book-form #book></book-form>
</mat-step>
<mat-step>
// ADDITIONAL STEPS ARE OMITTED FOR CLARITY
</mat-step>
</mat-horizontbal-stepper>
// CHILD COMPONENT CLASS
Component({
selector: 'book-form'
})
class BookFormComponent {
bookForm: FormGroup;
name = new FormControl('');
color = new FormControl('');
// Checkboxes for types of books a user can select. user can select multiple checkboxes
types = new FormArray([]);
optionsTypes = [];
ngOnInit(): void {
this.bookForm = this.fb.group({
name: this.name,
color: this.color
});
this.initializeTypesCheckboxes();
}
// This function will create 10 checkboxes that a user can select multiple of them
private initializeTypesCheckboxes() {
this.bookservice.getTypeOptions().subscribe(results => {
// the results from the server is array of strings of 10 elements
// eg: ["Maths", "English", "Chemistry", ...]
this.optionsTypes = results;
// we create checkboxes based on the number of types we get from the server
const typeCheckboxes = this.optionsTypes.map(t => new FormControl(false));
// we push the the checkboxes to the "this.types" form array
typeCheckboxes.forEach(type => this.types.push(type));
});
}
// This method is called from the parent component
public initBookData(bookData: BookData) {
this.naam.setValue(bookData.naam);
this.color.setValue(bookData.color);
this.initSelectedTypes(this.types, this.optionsTypes, bookData.selectedTypes);
}
// this method will use the already "alreadySelectedTypes" array to pre-select some of the checkboxes.
private initSelectedTypes(formArray: FormArray, optionsTypes: Array<string>, alreadySelectedTypes: Array<string>) {
for (let i = 0; i < formArray.controls.length; i++) {
for (const type of alreadySelectedTypes) {
if (optionsTypes === type) {
formArray.controls[i].patchValue(true);
}
}
}
console.log("LENGTH-formArray:", formArray.length); // i get O
console.log("LENGTH-optionsTypes:", optionsTypes.length); // i get O
}
}
What am I doing wrong?
Have you tried ContentChildren?
#ContentChildren(BookFormComponent) book: QueryList<BookFormComponent>;
It's unclear to me why you're using#ViewChildren at all. Unless I'm missing something about what you're trying to do, I think you're making your life more complicated than it needs to be.
The Parent Class
Your parent component class can be stripped down to just:
// PARENT COMPONENT CLASS
class LibraryComponent implements OnInit {
// ADDITIONAL CHILDREN FOR OTHER mat-step omitted for clarity
book: BookData = {};
form: FormGroup = new FormGroup()
ngOnInit(): void {
this.bookService.getBookData.subscribe(book => (this.book = book));
}
//This is to get the form group from a child Output event and use it in stepper.
onFormReady(form: FormGroup): void {
this.form = form;
}
}
Parent template
The way to pass data to child components from their parent is through an #Input directive. So, your parent template would look something like:
<mat-horizontal-stepper #stepper linear>
<mat-step [stepControl]="form">
<ng-template matStepLabel>Book</ng-template>
<book-form (formGroup)="onFormReady($event)" [bookData]="bookData"></book-form>
</mat-step>
<mat-step>
// ADDITIONAL STEPS ARE OMITTED FOR CLARITY
</mat-step>
</mat-horizontbal-stepper>
Child Class
Your child class can take care of setting up the form from its input, and then send the form group back up to the parent as a #Output. It would look something like this:
// CHILD COMPONENT CLASS
Component({
selector: 'book-form'
})
class BookFormComponent {
#Input('bookData') bookData: BookData
#Output('formGroup') formEmitter = new EventEmitter<FormGroup>();
bookForm: FormGroup;
options: string[]
ngOnInit() {
// get and store type options at start
this.booksService.getTypeOptions(options => {
// once options are ready.
// If options is empty, then the function on your service isn't working.
this.options = options;
this.bookForm = this.initializeForm(); // make the form
this.formEmitter.emit(this.bookForm); //send it to parent
})
}
initializeForm(): FormGroup {
const { name, color, selectedTypes } = this.bookData;
const form = this.fb.group({
name: new FormControl(name),
color: new FormControl(color),
types: new FormArray([])
})
// One form group for each possible option. Each group has a single control named after the option it represents.
this.options.forEach(option => {
let value = selectedTypes.includes(option);
form.types.push(this.fb.group({[option]: new FormControl(value)}));
})
return form;
}
}
I wouldn't expect this to work as is, but it's more or less the direction you should go in. It strips out a lot of the fat, makes your code easier to understand, and sends data between components the way you're supposed to.
The docs have a very good section that goes over the methods for doing this:
https://angular.io/guide/component-interaction

Array at index returning undefined in Angular ngAfterViewInit

Using Angular 7 and .NET Core 2.2, I'm working on a financial application and a watchlist for it. Symbols are added (and saved to a database). When the watchlist component loads, in ngOnInit, I get the data pulled from my resolver via router. Then I call a function which takes an array, loops through them, and returns a different data set from the financial data source. I then push each one of those returned items from the Observable into a local array watchListItems: IEXStockPrice[];
My code is:
ngOnInit() {
this.router.data.subscribe(data => {
this.watchListItemsFromDb = data['watchListItems'];
if (this.watchListItemsFromDb) {
this.fetchLatestPriceforWatchListItemsFromIEX(this.watchListItemsFromDb);
}
});
}
ngAfterViewInit(): void {
console.log(this.watchListItems);
console.log(this.watchListItems[0]);
}
fetchLatestPriceforWatchListItemsFromIEX(items: WatchListItemFromNet[]) {
if (!this.watchListItems) {
this.watchListItems = [];
}
items.forEach(element => {
this.iex.getStockQuoteBySymbol(element.symbol)
.subscribe(
(iexData: IEXStockPrice) => {
this.watchListItems.push(iexData);
},
(err: any) => this.alertify.error('Could not get the latest data from IEX.')
);
});
}
Now, I have a child component LineChartComponent which needs to create a line chart/graph of the active watchlist item. I was just going to first load the chart component with the first element in the watchListItems array. I can't get access to it in ngOnInit because the component hasn't finished initializing.
I've tried ngAfterViewInit.
However, as you can see in the picture, the watchListItems has content in it, but trying to access the first element returns undefined.
Since the router provides its data as an Observable, I would only call fetchLatestPriceforWatchListItemsFromIEX() if watchListItemsFromDb has been populated
ngOnInit() {
this.router.data.subscribe(data => {
this.watchListItemsFromDb = data['watchListItems'];
if (this.watchListItemsFromDb) {
this.fetchLatestPriceforWatchListItemsFromIEX(this.watchListItemsFromDb);
}
});
}
fetchLatestPriceforWatchListItemsFromIEX(items: WatchListItemFromNet[]) {
// initialize your array here vs in the viewmodel
if (!this.watchListItems) {
this.watchListItems = [];
}
items.forEach(element => {
this.iex.getStockBySymbol(element.symbol)
.subscribe(
(iexData: IEXStockPrice) => {
this.watchListItems.push(iexData);
},
(err: any) => this.alertify.error('Could not load watchlist')
);
});
}
Then in your component, you can check for the array, then pass the first item to your child component as an #Input:
<ng-container *ngIf="watchListItemsFromDb">
<child-component [data]="watchListItemsFromDb[0]"></child-component>
</ng-container>

Categories

Resources