I have a modal component with ng-content. In this ng-content I can pass other components with injector. I need to pass anyway an object inside it for editing data. But I need some help. This is the modal component html:
<div class="modal">
<div class="modal-body">
this is my Object: {{model | json}}
<ng-content></ng-content>
</div>
<div class="modal-footer">
<button (click)="sendMessage()">success</button>
<button>abort</button>
</div>
</div>
The components inside ng-content may vary. In my example I have an input. I need to pass a model inside that components.
I use a service with injector to pass the content into my modalComponent, and in the function "open" I defined a parameter obj that's the object that I need to see into ng-content component. (The parameter is stored inside "model" input on my Modal component).
#Injectable()
export class ModalService {
dialogComponentRef: ComponentRef<ModalComponent>;
private subject = new Subject<any>();
open(content: any, obj: any) {
const contentComponentFactory = this.cfResolver.resolveComponentFactory(content);
const modalComponentFactory = this.cfResolver.resolveComponentFactory(ModalComponent);
const contentComponent = contentComponentFactory.create(this.injector);
const modalComponent = modalComponentFactory.create(this.injector, [[contentComponent.location.nativeElement]]);
modalComponent.instance.model = obj;
this.dialogComponentRef = modalComponent;
document.body.appendChild(modalComponent.location.nativeElement);
this.appRef.attachView(contentComponent.hostView);
this.appRef.attachView(modalComponent.hostView);
}
}
This is my modalComponent Class:
export class ModalComponent {
#Input() model: any;
sendMessage() {
this.modalService.sendMessage();
}
constructor(private modalService: ModalService) {}
}
Therefore I defined a component with this html:
<input type="text" value="name" placeholder="name">
Inside this input I desire to see the object for editing this value.
I can't find a solution for having the object inside the content-component....I need it because I would have a form where I must edit some values.
This is my stackblitz example: Example
What you can do is assign the formData to a property of a service and then read that property in the content component's ngOnInit(), I tried below code and its working for me.
#Injectable()
export class ModalService {
dialogComponentRef: ComponentRef<ModalComponent>;
formData:any;
private subject = new Subject<any>();
open(content: any, obj: any) {
this.formData = obj;
const contentComponentFactory = this.cfResolver.resolveComponentFactory(content);
const modalComponentFactory = this.cfResolver.resolveComponentFactory(ModalComponent);
const contentComponent = contentComponentFactory.create(this.injector);
const modalComponent = modalComponentFactory.create(this.injector, [[contentComponent.location.nativeElement]]);
modalComponent.instance.model = obj;
this.dialogComponentRef = modalComponent;
document.body.appendChild(modalComponent.location.nativeElement);
this.appRef.attachView(contentComponent.hostView);
this.appRef.attachView(modalComponent.hostView);
}
}
and in ngOnInit() of content-component,
ngOnInit() {
this.name = this.modalService.formData.name;
}
However you can try to wrap this property in some function inside service, rather than accessing the property directly(getFormData() or something)
Related
I'm creating a shopping list.
It will be made out of two components: shopping-cart and shopped-item.
The shopping-cart has a button that dynamically adds a new shopped-item in a <div>.
The shopped-item after being added can be marked as active or unmarked so I created an EventEmmiter that changes the value marked/unmarked.
But since the component is added dynamically I don't know where to add it in shopping-cart component...
How can I make it work like this:
After the shopped-item is added it appears in an array with marked/unmarked value that changes when it's clicked in the shopped-item component?
Cheers!
Shopped-item.ts file:
export class ShoppedItemComponent implements OnInit {
_ref:any;
removeObject(){
this._ref.destroy();
}
#Output() statusChange = new EventEmitter<{status: boolean}>();
marked;
unmarkItem () {
this.marked = !this.marked;
this.statusChange.emit({status: this.marked});
}
constructor() {
}
}
Shopping-cart.ts file:
export class ShoppingCartComponent implements OnInit {
#ViewChild('boughtItems', { read: ViewContainerRef }) boughtItems:
ViewContainerRef;
constructor(
private resolver: ComponentFactoryResolver
) { }
isMarked = [];
shoppedItemStatus (statusChange: {status: boolean}) {
this.isMarked.push({
status: statusChange.status
})
}
addItem() {
const shoppedItem =
this.resolver.resolveComponentFactory(ShoppedItemComponent);
const component = this.boughtItems.createComponent(shoppedItem);
component.instance._ref = component;
}
}
Shopping-cart.html file:
<div #boughtItems>
<button (click)="addItem()">ADD</button>
</div>
Why are you creating the components by hand?
I would use a *ngFor in the view
<div #boughtItems>
<button (click)="addItem()">ADD</button>
<div *ngFor="let item of items">
<app-item-bought xxxx="item" (markToggle)="myFunction(item)"></app-item-bought>
</div>
</div>
where xxxx is a field of your class ShoppedItemComponent decorated with Input('xxxx').
(markToggle) is the name of the emitter in ShoppedItemComponent and myFunction(item) is a function defined in the Shopping-cart that will receive the item that has fired the event.
Hope it helps!
I have an Ionic application where I have created a component to show some data of an object. My problem is that when I update the data in the parent that hosts the component the data within the component does not update:
my-card.component.ts
#Component({
selector: 'my-card',
templateUrl: './my-card.html'
})
export class MyCard {
#Input('item') public item: any;
#Output() itemChange = new EventEmitter();
constructor() {
}
ngOnInit() {
// I do an ajax call here and populate more fields in the item.
this.getMoreData().subscribe(data => {
if (data.item){
this.item = data.item;
}
this.itemChange.emit(this.item);
});
}
}
my-card.html
<div class="comment-wrapper" *ngFor="let subitem of item.subitems">
{{subitem.title}}
</div>
And in the parent I use the component like this:
<my-card [(item)]="item"></my-card>
And the ts file for the parent:
#IonicPage()
#Component({
selector: 'page-one',
templateUrl: 'one.html',
})
export class OnePage {
public item = null;
constructor(public navCtrl: NavController, public navParams: NavParams) {
this.item = {id:1, subitems:[]};
}
addSubItem():void{
// AJAX call to save the new item to DB and return the new subitem.
this.addNewSubItem().subscribe(data => {
let newSubItem = data.item;
this.item.subitems.push(newSubItem);
}
}
}
So when I call the addSubItem() function it doesnt update the component and the ngFor loop still doesnt display anything.
You are breaking the object reference when you are making the api request. You are assigning new value, that is overwriting the input value you get from the parent, and the objects are no longer pointing to the same object, but item in your child is a completely different object. As you want two-way-binding, we can make use of Output:
Child:
import { EventEmitter, Output } from '#angular/core';
// ..
#Input() item: any;
#Output() itemChange = new EventEmitter();
ngOnInit() {
// I do an ajax call here and populate more fields in the item.
this.getMoreData(item.id).subscribe(data => {
this.item = data;
// 'recreate' the object reference
this.itemChange.emit(this.item)
});
}
Now we have the same object reference again and whatever you do in parent, will reflect in child.
If the getMoreData method returns an observable, this code needs to look as follows:
ngOnInit() {
// I do an ajax call here and populate more fields in the item.
this.getMoreData().subscribe(
updatedItem => this.item = updatedItem
);
}
The subscribe causes the async operation to execute and returns an observable. When the data comes back from the async operation, it executes the provided callback function and assigns the item to the returned item.
You declared item with #Input() decorator as:
#Input('item') public item: any;
But you use two-way binding on it:
<my-card [(item)]="item"></my-card>
If it is input only, it should be
<my-card [item]="item"></my-card>
Now if you invoke addSubItem() it should display the new added item.
this.item = this.getMoreData();
The getMoreData() doesn't make sense if you put it in your card component as you want to use the item passed via #Input()
Your component interactions are a little off. Check out the guide on the Angular docs (https://angular.io/guide/component-interaction). Specifically, using ngOnChanges (https://angular.io/guide/component-interaction#intercept-input-property-changes-with-ngonchanges) or use a service to subscribe and monitor changes between the parent and the child (https://angular.io/guide/component-interaction#parent-and-children-communicate-via-a-service).
I currently have this code in my app.component.ts
app.component.html
<div [ngClass]="myclass">
...rest of the content here
</div>
This I have the this:
<button (click)="changeClass('myFavClass')">Change Class to myFavClass</div>
app.component.ts
export class AppComponent {
myclass: string;
changeClass(myclass) {
this.myclass = myclass;
}
}
Now, all this works fine BUT I now want to put the triggering button on another component.
If I put this on another component:
<button (click)="changeClass('myFavClass')">Change Class to myFavClass</div>
How can I get it to change the class?
There are two ways you can do this you can use output with an EventEmit
Or you can set up a service that monitors the changes to a variable and use that as the control point for the change.
Personally, I use services for this instance as its easier to manage the code and its flow.
This answer has all the code in you need to look at.
Changing a value in two different components at the same time Angular 2
Hope that helps
There are at least two options. Subject and Observable or if this another component is a parent you can use #Input.
Subject and Observable method:
angular guide Highly recommended to read whole page.
Some component
export class SomeComponent {
constructor(private ClassService: ClassService) { }
private changeClass(class) {
this.ClassService.changeClass(class);
}
}
Another Component
export class AnotherComponent implements OnInit, OnDestroy {
constructor(private ClassService: ClassService) { }
private class: string = "";
private subscribtion: Subscribtion;
ngOnInit(): void {
this.Subscribtion = this.ClassService.someClass$.subscribe(
(class) => { this.class = class; }
)
}
ngOnDestroy(): void {
this.Subscribtion.unsubscribe();
}
}
Service
#Injectable();
export class ClassService{
constructor() { }
private someClassSource= new Subject<string>();
someClass$= this.someClassSource.asObservable();
changeClass(class) {
this.someClassSource.next(class);
}
}
taken from my answer
#Input method:
angular guide
This is very simple, when you click button changeClass method will change elClass which will be passed to another component by #Input decorator, every change of #Input will cause a detect changes which will detect that value has changed so class will change to myClass.
Parent component
parent.component.html
<another-component [elementClass]="elClass"></another-component>
<button (click)="changeClass('myClass')">change class<button>
parent.component.ts
export class ParentComponnet {
private elClass: string = "";
changeClass(class: string) {
elClass = class;
}
}
Another component (must be child component)
another.component.html
<div [ngClass]="elementClass">
another.component.ts
export class AnotherComponent {
#Input() elementClass: string;
}
There is also Child to Parent interaction via #Output (emitting event) angular guide
I have a component MyComponent and it is declared like this:
export class MyComponent implements IComponent {
...
#Input() Departments: any;
#Input() DropDownOptions: any;
#Input() Data: any[];
...
}
However, there is no property Data, when I try to access from PersonComponent component.
HTML of PersonComponent component:
<fieldset>
<my-comp #myGrid [Options]="ps.Options['myGrid']"></my-comp>
</fieldset>
TypeScript of PersonComponent component:
export class PersonComponent implements OnInit {
#ViewChild('myGrid') myGridComponent: MyComponent;
ngAfterViewInit() {
debugger;
let localData2 = this.myGridComponent.Data; // NO DATA PROPERTY. Undefined
}
ngAfterContentInit() {
debugger;
let localData1 = this.myGridComponent.Data; // NO DATA PROPERTY. Undefined
}
}
Variables that can be seen at debugger of Chrome:
How can I read values of Data property of MyComponent? What am I doing wrong?
#Input Data ... decorator "receives" data from the parent component. You set it via the attribute [Data] inside the parent template. If you don't set it it will be indefined. On the other hand you have [Options] attribute that doesn't have the corresponding #Input in the child.
You can fix it like so:
<fieldset>
<my-comp #myGrid [Data]="person.data"></my-comp>
</fieldset>
where person is an array with data field in parent component.
Please read thoughtfully the documention https://angular.io/guide/template-syntax#inputs-outputs and https://angular.io/guide/component-interaction#pass-data-from-parent-to-child-with-input-binding
And it would be better to not use reserved/too generic name like Data, Options to avoid name collisions and also camel case them.
I have a component which receives an array of image objects as Input data.
export class ImageGalleryComponent {
#Input() images: Image[];
selectedImage: Image;
}
I would like when the component loads the selectedImage value be set to the first object of the images array. I have tried to do this in the OnInit lifecycle hook like this:
export class ImageGalleryComponent implements OnInit {
#Input() images: Image[];
selectedImage: Image;
ngOnInit() {
this.selectedImage = this.images[0];
}
}
this gives me an error Cannot read property '0' of undefined which means the images value isn't set on this stage. I have also tried the OnChanges hook but I'm stuck because i can't get information on how to observe changes of an array. How can I achieve the expected result?
The parent component looks like this:
#Component({
selector: 'profile-detail',
templateUrl: '...',
styleUrls: [...],
directives: [ImageGalleryComponent]
})
export class ProfileDetailComponent implements OnInit {
profile: Profile;
errorMessage: string;
images: Image[];
constructor(private profileService: ProfileService, private routeParams: RouteParams){}
ngOnInit() {
this.getProfile();
}
getProfile() {
let profileId = this.routeParams.get('id');
this.profileService.getProfile(profileId).subscribe(
profile => {
this.profile = profile;
this.images = profile.images;
for (var album of profile.albums) {
this.images = this.images.concat(album.images);
}
}, error => this.errorMessage = <any>error
);
}
}
The parent component's template has this
...
<image-gallery [images]="images"></image-gallery>
...
Input properties are populated before ngOnInit() is called. However, this assumes the parent property that feeds the input property is already populated when the child component is created.
In your scenario, this is not the case – the images data is being populated asynchronously from a service (hence an http request). Therefore, the input property will not be populated when ngOnInit() is called.
To solve your problem, when the data is returned from the server, assign a new array to the parent property. Implement ngOnChanges() in the child. ngOnChanges() will be called when Angular change detection propagates the new array value down to the child.
You can also add a setter for your images which will be called whenever the value changes and you can set your default selected image in the setter itself:
export class ImageGalleryComponent {
private _images: Image[];
#Input()
set images(value: Image[]) {
if (value) { //null check
this._images = value;
this.selectedImage = value[0]; //setting default selected image
}
}
get images(): Image[] {
return this._images;
}
selectedImage: Image;
}
You can resolve it by simply changing few things.
export class ImageGalleryComponent implements OnInit {
#Input() images: Image[];
selectedImage: Image;
ngOnChanges() {
if(this.images) {
this.selectedImage = this.images[0];
}
}
}
And as another one solution, you can simply *ngIf all template content until you get what you need from network:
...
<image-gallery *ngIf="imagesLoaded" [images]="images"></image-gallery>
...
And switch flag value in your fetching method:
getProfile() {
let profileId = this.routeParams.get('id');
this.profileService.getProfile(profileId).subscribe(
profile => {
this.profile = profile;
this.images = profile.images;
for (var album of profile.albums) {
this.images = this.images.concat(album.images);
}
this.imagesLoaded = true; /* <--- HERE*/
}, error => this.errorMessage = <any>error
);
}
In this way you will renderout child component only when parent will have all what child needs in static content. It's even more useful when you have some loaders/spinners that represent data fetching state:
...
<image-gallery *ngIf="imagesLoaded" [images]="images"></image-gallery>
<loader-spinner-whatever *ngIf="!imagesLoaded" [images]="images"></loader-spinner-whatever>
...
But short answer to your questions:
When inputs are available?
In OnInit hook
Why are not available to your child component?
They are, but at this particular point in time they were not loaded
What can I do with this?
Patiently wait to render child component utul you get data in asynchronous manner OR learn child component to deal with undefined input state