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
Related
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?
}
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
I'm fetching data from my service in the app component, and then passing it down to a child component via #Input. when I console.log the data in ngOnInit, it DOES show up in the child component, but when I try to pass it to a variable and use it in the child template, it's coming back undefined, and I'm not sure why.
Here's where I call my service in app.component.ts, the data that is having issues is this.colorcounts. Console logging here inside the ngOnInit DOES show the correct data. colorCounts has 3 properties: red, yellow & green:
ngOnInit() {
this.pciService.getPciInfo()
.subscribe((pciInfo) => {
this.spinner.hide();
this.pciData = pciInfo.sorted,
this.colorCounts = pciInfo.counts;
});
Here's the app.component.html template where I'm passing the data down:
<app-navbar *ngIf="colorCounts" [colorCounts] = "colorCounts"></app-navbar>
<app-system-status [pciData]="pciData"></app-system-status>
Here's the child component where I'm grabbing the data, doing a console.log in ngOnInit does work, but trying to use "red" in the template or save it in the class comes back undefined:
constructor(private pciService: PciService,
public dialog: MatDialog,
private decimalPipe: DecimalPipe) { }
AMVersion: any;
#Input() colorCounts: Colors;
openDialog(): void {
let dialogRef = this.dialog.open(AmVersionDialogComponent, {
panelClass: 'custom-dialog-container',
data: {}
});
(<AmVersionDialogComponent>dialogRef.componentInstance).AMVersion = this.AMVersion;
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
});
}
ngOnInit() {
this.pciService.getAMVersion()
.subscribe((AMInfo) => {
return this.AMVersion = AMInfo;
});
var red = this.colorCounts.red;
console.log(red)
console.log(this.colorCounts);
}
}
I know I'm probably missing something simple regarding the life cycle. Can someone point me in the right direction here?
All Observables are async so in template *ngIf condition will check variable and if it is null will return null but if You pipe variable as | async it will be checking this variable all time and when variable will apear not null it will show content ngIf.
*ngIf works only once !!! He not waiting for anny http calls and Observables are making one usualy. If You want to *ngIf wait for calls You need to use | async pipe inside.
Same as You subscribe to it in ngOnInit() and assign to variables in template. Subscription will assign those values later after template is allredy on screen. Read about what async means.
You need to know that this is a boilercode:
ngOnInit() {
this.pciService.getPciInfo()
.subscribe((pciInfo) => {
this.spinner.hide();
this.pciData = pciInfo.sorted,
this.colorCounts = pciInfo.counts;
});
It is better to do it like this:
ngOnInit() {
this.pciInfo$ = this.pciService.getPciInfo()
}
in template:
<ng-container *ngIf="pciInfo$ | async as pciInfo">
<app-navbar [colorCounts]="picInfo.counts"></app-navbar>
</ng-container>
Pipe Async will subscribe for you to an Observable.
Is there a way to print a firestore object? While working with AngularFire i'm attempting to print to html the name of the document in the firestore. My firestore has a parent "nforms" which has two children as documents "pdf","img", inside pdf i have one pdf called "norcal-letterhead.pdf", inside img i have one img called "git-img.jpg".
This is how I am pulling the data form the firestore.
fptr: Observable<any[]>;
fileData: DocumentReference[];
fileCollection: DocumentReference;
filePtr: Observable<any>;
constructor(firestore: AngularFirestore, firestorage: AngularFireStorage) {
this.filePtr = firestore.collection('nforms').valueChanges();
this.filePtr.subscribe(
data => {
this.fileData = data
console.log(this.fileData)
this.fileData.map(r => this.fileCollection = r)
console.log(this.fileCollection)
}
);
}
This is how I am printing the data from the pull.
<tr *ngFor="let data of filePtr|async">
<td *ngIf="fileCollection; let a">{{a}} or {{data}}</td></tr>
I'm getting back this: [object Object] or [object Object]
This is the response that i'm getting from the console. As you can see i'm able to pull what looks like an Array of 2 with objects of type DocumentReference.
I'd like to share some more insigh on this post for future reference to whomever it could help. While this isn't the answer to printing the firestore objects that were created from using the console 'upload' button, this is an answer to printing the same objects except this time i created them through the sdk '.upload'.
Define the object FireStore Item:
export interface fstoreitem{
form_id: string;
form_name: string;
form_path: string;
}
Define the firestore reference to a collection nforms:
constructor(private firestore: AngularFirestore){
this.fitemCollection = firestore.collection<fstoreitem>('nforms/');
this.fitem = this.fitemCollection.valueChanges();
}
Define the routine to upload a firestore item:
addStoreItem(form_name: string, form_path: string) {
const form_id = this.firestore.createId();
const form_item: fstoreitem = { form_id, form_name, form_path };
this.fitemCollection.doc(form_id).set(form_item);
}
Display the objects in the view:
<tr *ngFor="let data of fitem | async">
<td>{{data.form_name}}</td>
<td>download</td>
</tr>
In my data I have an object program. I load a list of objects asynchronously and add it to program.sections.
async loadSections() {
if (this.user == null && this.program.sections) { return }
await this.$store.dispatch('loadProgramSections', {program: this.program, user: this.user});
console.log('loaded Sections', this.program);
// this.$set(this.program, 'sections', this.progrmm.sections)
// this.$nextTick(() => { this.$forceUpdate(); })
},
My UI looks like this:
<Section :program="program" ></Section>
So I pass my program object down to my Sections component
In my console log I can see that the field programm.sections is indeed my array of objects but my UI does not get changed. When I put my UI code from the Sections component directly in to my page, it gets updated and the data is dispalyed correctly but when I use the component the props are not being updated correctly.
I already tried the commented lines in my code but it doesn't work.
Any Ideas?
Assign a new object reference in this.program:
async loadSections() {
if (this.user == null && this.program.sections) { return }
await this.$store.dispatch('loadProgramSections', {program: this.program, user: this.user});
console.log('loaded Sections', this.program);
// assign a new object reference
this.program = Object.asssign({}, this.program);
},
Also make sure you initialize program in data() section.
The main issue is that Vue cannot detect the changes in some situations.
e.g. set a item to an array or set an additional property to an object
That's the crux of the problem. You might need Caveats.