-
Notifications
You must be signed in to change notification settings - Fork 6.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(dialog): add ability to open dialog with templateRef #2853
Conversation
Fixed tests and added separated demo dialog components because of the fact that dialog opened with |
return attachResult; | ||
} | ||
|
||
attachTemplateRef<T>(templateRef: TemplateRef<T>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TemplateRef
should be instantiated in dialog.ts
and we should just call attach
on the dialog container (from BasePortalHost
) that will delegate to the appropriate method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand this correctly, but to be able to create a portal, you need ViewContainerRef, which is not accessible inside dialog.ts
. In my previous PR I had MdDialog.openFromPortal()
, but I think it a little too tedious for user to have to create a portal rather than to be just able to pass in TemplateRef
. But I don't really mind either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, I could make ViewTemplateRef
available to dialog.ts
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about types thought, wouldn't you lose them by just calling attach
instead of attachComponentPortal
/attachTemplatePortal
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also if I just use attach
, typescript will complain about not properly implementing attachTemplatePortal
and attachComponentPortal
.. what should I do please, @jelbourn ?
backdropClicked: Observable<void>; | ||
|
||
/** Subject for notifying the user that esc key way pressed */ | ||
escapePressed = new Subject<void>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: the Subject
shouldn't ever be a public property, only the Observable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
asObservable()
then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that I need to next
it from inside dialog-container.ts
, if I make it private I won't be able to access it.
@@ -14,10 +14,18 @@ export class MdDialogRef<T> { | |||
/** The instance of component opened into the dialog. */ | |||
componentInstance: T; | |||
|
|||
/** Expose overlay backdrop click event */ | |||
backdropClicked: Observable<void>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be backdropClick
(to match OverlayRef
).
I don't think there needs to be an API for escapePressed
. Users should be able to add their own keydown
or keypress
handler to their dialog content.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I thought first as well, but then there is afterClosed
instead of afterClose
, but I was not sure.
As for escape, there is handleEscapeKey()
on dialog-container.ts
already, so I think it's fair to expose that.
* @param config Extra configuration options. | ||
* @returns Reference to the newly-opened dialog. | ||
*/ | ||
openFromTemplateRef<T>(templateRef: TemplateRef<T>, config?: MdDialogConfig): MdDialogRef<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just openFromTemplate
openFromTemplateRef<T>(templateRef: TemplateRef<T>, config?: MdDialogConfig): MdDialogRef<T> { | ||
config = _applyConfigDefaults(config); | ||
|
||
let overlayRef = this._createOverlay(config); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than duplicating the control flow, open
and openFromTemplate
should just instantiate the appropriate Portal
and then pass that through. One of the benefits of portals is that the host doesn't have to care which kind it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar problem as with #2853 (comment) I don't think I can do that unless I have ViewContainerRef
, which I don't unless I require the user to pass me TemplatePortal
.. which I think would be quite redundant anyway, since dialog-container's ViewContainerRef
is that I really need anyway, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I get the issue here. Isn't it down to doing a ref instanceof TemplateRef
check and then taking the proper route under the hood?
* @param config Extra configuration options. | ||
* @returns Reference to the newly-opened dialog. | ||
*/ | ||
openFromTemplateRef<T>(templateRef: TemplateRef<T>, config?: MdDialogConfig): MdDialogRef<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this method should lose the generic and return MdDialogRef<void>
. since the generic refers to the type of componentInstance
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was not sure about what to do with that(#2851 read 2.).
backdropClicked: Observable<void>; | ||
|
||
/** Subject for notifying the user that esc key way pressed */ | ||
escapePressed = new Subject<void>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that we need observables for escapePressed
, backdropClicked
and afterClosed
since the API is already becoming pretty dense. I'd rather we kept everything to afterClosed
and emitted an object that looks something like { result: <user-provided value>, source: 'backdrop' | 'keypress' | 'api' }
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That wouldn't trigger if escape/backdrop click didn't actually close the dialog. Which in result wouldn't allow for creating a stateless dialog component(which is honestly a must have in redux and ngrx/store world). Or afterClosed
could be modified to trigger and not close the dialog when disableClose
is true, but then the name wouldn't make much sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EDIT: I see what you mean, but why would the user want to know that the backdrop was clicked and it didn't result in a close?
You could differentiate it based on the source. My worry is that we're ending up with 5-6 different dialog-related observables. If we added these, we'd have 3 just for the close
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What could I differentiate? The problem is that afterClosed()
triggers only when close
is called, which actually closes the dialog . I need to be able to catch any close attempt while disableClose
is true
and let user know about it, so he can modify the state himself. Would you be happier is backdrop and escape were merged into one observable, say closeAttempt
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My question is why does the user need to know that an attempt to close it was made?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check #2855
Imagine you keep state of your modal in a global store, like redux or @ngrx/store, so you create a component that looks like this.
<my-dialog [open]="modalState$ | async">
<button (click)="doSomethingThatChangesModalState$toFalse()">close</button>
</my-dialog>
Now you are in full control of the state, when you change modalState$
, your modal closes, BUT when you click on backdrop or press escape, your modal closes while your modalState$
is still true and your store is suddenly out of sync. You could catch up by changing modalState$
to false
on afterClose()
, but that could for example be called only after the animation finishes. It simply smells, it's the other way around than it should be. So you want to disable the default closing behaviour of backdropClick
and escape
, so you do disableClose: true
, but then you have to manually set up listeners for both, which will be big pain in the ass(or it might not even be possible at all).
So ideally you want to do
<my-dialog
[open]="modalState$ | async"
(close)="doSomethingThatChangesModalState$toFalse()">
<button (click)="doSomethingThatChangesModalState$toFalse()">close</button>
</my-dialog>
where close event happens on either backdropClick
or escape
press https://github.com/fxck/material2/blob/662703b63831797c9969dda4dd03d69d794aa1f4/src/lib/dialog/dialog-element.ts#L54-L61
It's what's called stateless components and what you want to do when using redux or store. There are other components where this would make sense, for example keeping sidenav state inside ngrx/store is currently a bit of a pain in the ass as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, it makes sense. I still think that they should be combined into something along the lines of userGeneratedClose
.
openFromTemplateRef<T>(templateRef: TemplateRef<T>, config?: MdDialogConfig): MdDialogRef<T> { | ||
config = _applyConfigDefaults(config); | ||
|
||
let overlayRef = this._createOverlay(config); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I get the issue here. Isn't it down to doing a ref instanceof TemplateRef
check and then taking the proper route under the hood?
let dialogRef = this._attachDialogContent( | ||
dialogContainer, | ||
overlayRef, | ||
undefined, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably better to pass in null
here instead. That way we know that it was skipped on purpose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://medium.com/@basarat/null-vs-undefined-in-typescript-land-dc0c7a5f240a#.ug5uqgtus but sure, I guess rest of the lib is not following this anyway
@@ -143,10 +174,16 @@ export class MdDialog { | |||
let userInjector = config && config.viewContainerRef && config.viewContainerRef.injector; | |||
let dialogInjector = new DialogInjector(dialogRef, userInjector || this._injector); | |||
|
|||
let contentPortal = new ComponentPortal(component, null, dialogInjector); | |||
if (component) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the logic that is specific to TemplateRef
and ComponentType
should be split into separate private methods.
|
||
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; | ||
|
||
let clicked = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of keeping a flag like this, you should use a spy:
let clickedSpy = jasmine.createSpy('clicked callback');
dialogRef.backdropClicked.subscribe(clickedSpy);
expect(clickedSpy).toHaveBeenCalled();
let dialogContainer: MdDialogContainer = | ||
viewContainerFixture.debugElement.query(By.directive(MdDialogContainer)).componentInstance; | ||
|
||
let pressed = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should use a spy as well. See https://github.com/angular/material2/pull/2853/files#r98516864
Addressed couple of comments:
Didn't address these:
|
super(); | ||
|
||
this.containerRef = this._viewContainerRef; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
had to do this, when I did just
constructor(viewContainerRef: ViewContainerRef, private _ngZone: NgZone) {}
this https://github.com/fxck/material2/blob/9a0a28be6b551302363577462865fac760ad09a3/src/lib/dialog/dialog.ts#L212 was complaining that viewContainerRef
doesn't exist
@fxck regarding your comment about not being able to expose the |
I could probably base my PR on yours if you want. I actually was wondering about another thing regarding your PR, it currently doesn't take in account from what button was the dialog opened(like material1 does), but once that's there, I'm not sure how's that gonna work with element like this #2855 |
There isn't anything in the spec that says that it's supposed to do that, and I couldn't find anybody that remembered why it was done that way in M1. The only reference to dialog animations in the spec is this upon which I based my PR. After looking through different implementations (Google Docs, Google Keep, Inbox, Android) it's pretty inconsistent. |
I think it might have something to do with one of these https://storage.googleapis.com/material-design/publish/material_v_10/assets/0B14F_FSUCc01dFhwekx5R3BsNGM/Radial_04_RadialArc_v5-remapped.mp4 .. it's gotta be said it arguably looks pretty good in m1.. Anyway, how should I proceed with this PR then? I'll do anything you want/need as long as it allows me to create that stateless dialog component/directive. I would really love to get this in, it's been bothering me for a long long time. |
I would say to go with the assumption from #2825 and if we decide to go the M1 route, I think that it's not too complicated to refactor. |
I guess I'll wait until that gets merged then. |
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
I split #2851, used
TemplateRef
instead ofTemplatePortal
, and committed only the part that addsopenFromTemplateRef()
.Added tests for
backdropClicked
,escapePressed
(I explained the need for these in the previous PR) and a test for creating a dialog using templateRef. Not sure which other tests would be needed, I think the rest is identical to opening a dialog withopen()
.Then there's a little "problem" that components inside the
<template>
do not have access toMdDialogRef
, sodialogRef.close()
andmd-dialog-close
obviously do not work, but if you are opening a dialog usingTemplateRef
, you likely do not need those anyway.@jelbourn @crisbeto @devversion