primeng icon indicating copy to clipboard operation
primeng copied to clipboard

Dynamic Dialog: Pass Inputs to Component (@Input)

Open daleclements opened this issue 4 years ago • 11 comments

I'm submitting a ... (check one with "x")

[ ] bug report => Search github for a similar issue or PR before submitting
[X] feature request => Please check if request is not on the roadmap already https://github.com/primefaces/primeng/wiki/Roadmap
[ ] support request => Please do not submit support request here, instead see http://forum.primefaces.org/viewforum.php?f=35

As far as I can tell, there is currently no way to pass inputs to a component in a Dynamic Dialog opened by the DialogService. I know I can pass data, but I'm talking about inputs specified with @Input(). This would allow use of components that are not specifically written just for dialog use to be displayed in a dialog or a template depending on which is appropriate at the time.

For example: Let's say I have a cars demo component that I show in a template somewhere like this:

<cars-demo-component [cars]="cars"></cars-demo-component>

and I want to show it in a dialog somewhere else:

const ref = this.dialogService.open(CarsDemoComponent, {//options...});

I don't see any way, currently, to pass a [cars] input to the component in the dialog. The component inside the dialog must be written specifically for use within a dialog.

The best workaround I can think of now is to create a CarsDemoDialogWrapperComponent to adapt the CarsDemoComponent for Dynamic Dialog use. If this is deemed a sufficient solution, I think an example added to the documentation would be useful.

daleclements avatar Dec 05 '19 19:12 daleclements

It could be done as its done on NebularUI, passing context to the modal: https://akveo.github.io/nebular/docs/components/dialog/overview#nbdialogservice

mikaelboff avatar Jun 15 '20 13:06 mikaelboff

Further, at risk of feature creep, we should be able to subscribe to the underlying component's @Outputs as well:

ref.carClicked.subscribe(() => {})

Ng-Zorro Antd does this in their modal, where you can pass component @Input when creating your modal / dialog. Once you create it, then you can get a reference to the component inside the dialog and subscribe to it.

PaddingtonTheBear avatar Nov 17 '20 08:11 PaddingtonTheBear

Access to dialog @Output is really required. Its implemented a long time ago in angular material dialogs. And it's useful quite often. this.myDialogRef.componentInstance.submitClicked.subscribe(...) Where submitClicked is an @Output of dialog component. Currently, we can emit results by closing the dialog. But often there is a use case where you need for example to send a request to the backend and only then close the dialog and send some event to the outer level.

Halynsky avatar Apr 20 '22 15:04 Halynsky

Any updates on @Output ?

pasevin avatar May 30 '22 08:05 pasevin

For anyone interested, I created a very basic custom implementation to make this work. It will assign all props provided in config.data to the component. Note that it does not check if props in component are marked with @Input. Also it will be applied only once, when the dialog is being opened.

export class DynamicDialogComponent extends DynamicDialogComponentBase {

	@ViewChild(DynamicDialogContent) insertionPoint: DynamicDialogContent;

	loadChildComponent(componentType: Type<any>) {
		super.loadChildComponent(componentType);

		// TODO: replace when https://github.com/primefaces/primeng/issues/8383 is solved.
		// simple custom implementation for passing data object to component inputs
		if(this.config.data) {
			const data = this.config.data;
			const comp = this.componentRef.instance;
			// eslint-disable-next-line guard-for-in
			for(const key in data) {
				if(typeof data[key] !== 'undefined') comp[key] = data[key];
			}
			this.componentRef.changeDetectorRef.markForCheck();
		}
	}
	
}

jbjhjm avatar May 30 '22 10:05 jbjhjm

Can we bump this feature request?

The dialog needs far more than access to the data sent once, when it was opened. Let's get this fully functional with the basic expectations of a reusable dialog component.

  1. get data
  2. display data
  3. update data
  4. inform its parent of changes/data

Hopefully this helps someone. Currently I use the dialogService.open(SomeComponent, { propName: 'some data' ... to open the modal and pass it static data. Then I create a provided service dedicated to all changes for this specific dialog instance.

BenRacicot avatar Aug 03 '22 17:08 BenRacicot

Here is my workaround. I hope it will help somebody =)

export class ConfirmDialogComponent {
  @Output() onConfirm: EventEmitter<void>
  @Output() onCancel: EventEmitter<void>

  constructor(public dialogRef: DynamicDialogRef,
              public config: DynamicDialogConfig) {
    this.onConfirm = this.config.data.onConfirm || new EventEmitter<void>()
    this.onCancel = this.config.data.onCancel || new EventEmitter<void>()
  }

  confirm() {
    this.onConfirm.emit()
  }

  cancel() {
    this.dialogRef.close()
    this.onCancel.emit()
  }
}
// Some top-level component with your Confirm button
confirm() {
  const onConfirm: EventEmitter<void> = new EventEmitter<void>()
  const dynamicRef = this.dialogService.open(ConfirmDialogComponent, {
    header: `Are you sure that you are sure?`,
    data: {onConfirm},
    width: '500px'
  });

  onConfirm.subscribe(() => {
    this.snackBarService.showSuccess(`What is done that is done`)
    dynamicRef.close()
  })
}

The main problem is that the data object is not type-safe. So you can not restrict or force users to write correct code. And to have these emitters references on an outside level you have to pass them from the outer level.

Halynsky avatar Aug 12 '22 17:08 Halynsky

Note that since angular 14.1 there's an "official" way to set input values, so now we can do it like this:

if(this.config.data) {
	for(const key in this.config.data) {
		this.componentRef.setInput(key,this.config.data[key]);
	}
}

It will also trigger change detection automatically, so less things to take care of!

jbjhjm avatar Sep 15 '22 11:09 jbjhjm

Note that since angular 14.1 there's an "official" way to set input values, so now we can do it like this:

if(this.config.data) {
	for(const key in this.config.data) {
		this.componentRef.setInput(key,this.config.data[key]);
	}
}

It will also trigger change detection automatically, so less things to take care of!

Nice input binding solution! One problem solved.

But still no default way to pass data out, correct?

BenRacicot avatar Sep 20 '22 18:09 BenRacicot

Note that since angular 14.1 there's an "official" way to set input values, so now we can do it like this:

if(this.config.data) {
	for(const key in this.config.data) {
		this.componentRef.setInput(key,this.config.data[key]);
	}
}

It will also trigger change detection automatically, so less things to take care of!

Nice input binding solution! One problem solved.

But still no default way to pass data out, correct?

Nope, no official solution. But with reflectComponentType you can get info on decorated outputs. Subscribe to it after creating the component. Make sure to clean up on destroy. That should be all that is required to make it work.

jbjhjm avatar Sep 21 '22 08:09 jbjhjm

Hey @jbjhjm after researching reflectComponentType I cannot understand how we can access or watch an @output emit within the parent component. Can you show us how you use it in this way?

BenRacicot avatar Sep 21 '22 19:09 BenRacicot

@BenRacicot reflectComponentType allows you to get a list of which properties are decorated as Outputs. Technically, these outputs are just Observables (with some extra functionality, but nontheless). So you'd do something like this:

const outputNames = reflectComponentType(MyComponent).outputs
outputNames.forEach(prop=>{
  const outputProp$ = componentRef.instance[prop.propName];
  outputProp$.subscribe(output=>{...});
  // remember to add unsubscribe logic in your preferred way!
}


jbjhjm avatar Sep 22 '22 08:09 jbjhjm

Hi,

So sorry for the delayed response! Improvements have been made to many components recently, both in terms of performance and enhancement. Therefore, this improvement may have been developed in another issue ticket without realizing it. You can check this in the documentation. If there is no improvement on this, can you reopen the issue so we can include it in our roadmap? Please don't forget to add your feedback as a comment after reopening the issue. These will be taken into account by us and will contribute to the development of this feature. Thanks a lot for your understanding!

Best Regards,

mertsincan avatar Nov 09 '22 19:11 mertsincan

Should not have been closed as far as i'm aware there's still no way to do this in PrimeNG you have to write boilerplate like this everytime and pass in the data obj.

        this.intakeMode = this.configModal.data.intakeMode;
        this.challengeStudy = this.configModal.data.challengeStudy;

https://ng-bootstrap.github.io/#/components/modal/api - allows you to access the underlying component instance to set inputs. Seems like a lot of angular libs support this functionality in one way or another that is a bit more elegant than what PrimeNG is currently offering. If there is currently some better way to do this please let me know.

Side node - On the primeng website there's not even docs anymore for dynamic dialog? - EDIT: It was fixed

JacobSiegle avatar Mar 28 '23 13:03 JacobSiegle

Do I understand correctly that there is still no easy way to bind values for @Input and @Output parameters of the displayed component when defining a Dynamic Dialog?

In fact, without customizing the displayed component specifically for a Dynamic Dialog (using the implementation of DynamicDialogConfig), this cannot be done

kazakevich-alexei avatar Oct 09 '23 19:10 kazakevich-alexei

in fact, this is not a full-fledged component, but a small wrapper for convenience, for the chickens to laugh, mockery.

kazakevich-alexei avatar Oct 09 '23 20:10 kazakevich-alexei

Here is my workaround. I hope it will help somebody =)

export class ConfirmDialogComponent {
  @Output() onConfirm: EventEmitter<void>
  @Output() onCancel: EventEmitter<void>

  constructor(public dialogRef: DynamicDialogRef,
              public config: DynamicDialogConfig) {
    this.onConfirm = this.config.data.onConfirm || new EventEmitter<void>()
    this.onCancel = this.config.data.onCancel || new EventEmitter<void>()
  }

  confirm() {
    this.onConfirm.emit()
  }

  cancel() {
    this.dialogRef.close()
    this.onCancel.emit()
  }
}
// Some top-level component with your Confirm button
confirm() {
  const onConfirm: EventEmitter<void> = new EventEmitter<void>()
  const dynamicRef = this.dialogService.open(ConfirmDialogComponent, {
    header: `Are you sure that you are sure?`,
    data: {onConfirm},
    width: '500px'
  });

  onConfirm.subscribe(() => {
    this.snackBarService.showSuccess(`What is done that is done`)
    dynamicRef.close()
  })
}

The main problem is that the data object is not type-safe. So you can not restrict or force users to write correct code. And to have these emitters references on an outside level you have to pass them from the outer level.

Thanks for sharing this solution, works like a charm. Nevertheless, I would prefer a "real" implementation in PrimeNG.

MAN-Sendance avatar Oct 12 '23 10:10 MAN-Sendance

So until this point, do we have an official solution for inputs(), that was the main purpose of the thread,? or do we need to use the tricky binding solution, I prefer not to inject DynamicDialogConfig to keep the component clean, not all the time is needed to open using the dynamicDialogService.

Note that since angular 14.1 there's an "official" way to set input values, so now we can do it like this:

if(this.config.data) {
	for(const key in this.config.data) {
		this.componentRef.setInput(key,this.config.data[key]);
	}
}

It will also trigger change detection automatically, so less things to take care of!

Nice input binding solution! One problem solved.

But still no default way to pass data out, correct?

adamofig avatar Nov 11 '23 19:11 adamofig