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
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 am doing simple todo app. I have 1 input box and Add button. On typing into input box and clicking Add button the text is displayed in a list below. I am using below constructor:
class Entry extends Component {
constructor() {
super()
this.state = {
st_search_field: '',
st_lists: [
{
list_id: new Date(`enter code here`),
list_name: ''
}
]
}
}
Now I create list_handler = () => { ...... } function to set search_field text into list_name value. I use list_handler method during onClick for Add button. I have started by usingconst join = Object.assign({}, this.state) in list_handler function and tried using this.setState({st_lists.list_name: this.state.st_searh_field}) butst_lists.list_name is marked in red in VS editor. Tried this.state.st_lists.map(li => {}) above setState method but even that gives error.
you can't set state on one of states sub objects do it like this:
const st_lists = this.state.st_lists;
st_lists.list_name = this.state.st_search_field;
this.setState({st_lists});
st_lists.list_name is not a key, you need to add merge the object
this.setState({
st_lists: [
{
...this.state.st_lists,
list_name: this.state.st_searh_field,
},
]
})
I have a 'Schedule' typescript class and a 'selectedSchedule' variable of this 'Schedule' type.
Schedule:
export class Schedule{
scheduleId: string;
sheduleDate: Date = new Date();
scheduleName: string;
jobs: Job[];
unscheduledEmployeesCount: number = 0;
idleEquipmentCount: number = 0;
unscheduledTrucksCount: number = 0;
}
I'm binding this variable and it's properties in the HTML by utilizing interpolation. My problem is I'm using a *ngFor to iterate and display each 'job'...
<div class="job" *ngFor="let j of selectedSchedule.jobs">
<div [ngClass]="{'hasEmployees': j.jobEmployees.length > 0 }">
<div *ngFor="let e of j.jobEmployees">
{{e.employee['name'] !== null ? e.employee['name'] : e.name}}
</div>
</div>
</div>
whenever the user triggers the 'addJob()' method by clicking a button, the newly created 'job' doesn't get detected by Angular and results in the properties of the 'job' to be null, most notably the 'jobEmployees' of each 'job'. I understand Angular doesn't detect changes to an array when you push/remove objects out of the box. I'm looking for a solution to pick up on these changes when a user adds a new job. I have tried to reassign the 'selectedSchedule' and it's 'jobs' property with no success. Also have tried to slice() the jobs array with the thought of 'recreating' the array.
I'm currently making an extra http request to get the schedule's jobs again, which seems to prevent the null property problems, but I'm hoping to find a more efficient solution.
The addJob() method in component:
addJob() {
var newJob: Job;
this.data.addJob(this.selectedSchedule.scheduleId).subscribe(addedJob => {
this.selectedSchedule.jobs.push(addedJob);
this.data.loadSchedules().subscribe(success => {
if (success) {
this.selectedSchedule = this.data.schedules.find(schedule => schedule.scheduleId === this.selectedSchedule.scheduleId);
}
});
});
}
addJob() method in data service:
addJob(scheduleId: string) {
return this.http.get("/api/job/addjob", { headers: this.headers, params: { scheduleId: scheduleId } })
.map((job: Job) => {
return job;
});
}
'Job' class:
export class Job {
jobId: string;
jobName: string;
jobNumber: string;
jobEmployees: Array<Employee> = new Array<Employee>();
jobEquipment: Array<Equipment> = new Array<Equipment>();
jobTrucks: Array<Truck> = new Array<Truck>();
}
You are looking for ChangeDetectorRef,Normally this is done by Angulars change detection which gets notified by its zone that change detection should happen. Whenever you manually subscribe in a component, angular marks the component instance as dirty whenever the subscribed stream emits a value.
Try calling it manually as follows,
this.selectedSchedule = this.data.schedules.find(schedule => schedule.scheduleId === this.selectedSchedule.scheduleId);
this.cdRef.detectChanges();
I have main component with this code(without imports):
class AppComponent {
products = null;
productsUpdated = new EventEmitter();
constructor(product_service: ProductService) {
this._product_service = product_service;
this._product_service.getList()
.then((products) => {
this.products = products;
this.productsUpdated.emit(products)
});
}
}
With template:
<left-sidenav [productsReceived]="productsUpdated"></left-sidenav>
And component for sorting products:
class LeftSidenavComponent {
#Input() productsReceived;
#Output() productsSorted = new EventEmitter();
categories = []
constructor(product_list_service: ProductListService) {
this._product_list_service = product_list_service;
}
ngOnInit() {
this.productsReceived.subscribe((products) => {
this.categories = products.map((elem) => {
return elem.category
})
});
}
}
So when all is drawn, categories array in LeftSidenavComponent is empty.
I think that productUpdated event emitter fires earlier than LeftSidenavComponent subscribes to it, but don't know how to handle that.
I would recommend moving the EventEmitter's to the service you have injected like
#Injectable()
export class DrinksService {
drinkSelected = new EventEmitter<any>();
drinkChanged = new EventEmitter<any>();
drinksToggle = new EventEmitter<any>();
}
The above code is an example from one of my projects, but just change the variable names.
This way rather then relying on the HTML template to modify productsReceived, you simply subscribe to the eventEmitters in ngOnInit.
Currently, your code is using databinding to an event emitter productsUpdated but you could simply databind [productsReceived]="productsUpdated" where productsUpdated is an empty list. Once productsUpdated is populated with values, it will be reflected in the DOM. You have to populate productsUpdated by subscribing to an event emitter like...
this.myEmitter
.subscribe(
(data)=>this.productsUpdated = data
);
Does this help? The main thing is to databind to a list, and not an event emitter.
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.