Skip to content
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

Closed
wants to merge 6 commits into from

Conversation

fxck
Copy link
Contributor

@fxck fxck commented Jan 28, 2017

I split #2851, used TemplateRef instead of TemplatePortal, and committed only the part that adds openFromTemplateRef().

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 with open().

Then there's a little "problem" that components inside the <template> do not have access to MdDialogRef, so dialogRef.close() and md-dialog-close obviously do not work, but if you are opening a dialog using TemplateRef, you likely do not need those anyway.

@jelbourn @crisbeto @devversion

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Jan 28, 2017
@fxck
Copy link
Contributor Author

fxck commented Jan 29, 2017

Fixed tests and added separated demo dialog components because of the fact that dialog opened with TemplateRef does not have MdDialogRef.

return attachResult;
}

attachTemplateRef<T>(templateRef: TemplateRef<T>) {
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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>();
Copy link
Member

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asObservable() then?

Copy link
Contributor Author

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>;
Copy link
Member

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.

Copy link
Contributor Author

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> {
Copy link
Member

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);
Copy link
Member

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.

Copy link
Contributor Author

@fxck fxck Jan 30, 2017

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?

Copy link
Member

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> {
Copy link
Member

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.

Copy link
Contributor Author

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>();
Copy link
Member

@crisbeto crisbeto Jan 30, 2017

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' }.

Copy link
Contributor Author

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.

Copy link
Member

@crisbeto crisbeto Jan 30, 2017

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.

Copy link
Contributor Author

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?

Copy link
Member

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?

Copy link
Contributor Author

@fxck fxck Jan 30, 2017

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.

Copy link
Member

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);
Copy link
Member

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,
Copy link
Member

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.

Copy link
Contributor Author

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) {
Copy link
Member

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;
Copy link
Member

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fxck
Copy link
Contributor Author

fxck commented Jan 30, 2017

Addressed couple of comments:

Didn't address these:

@jelbourn @crisbeto

super();

this.containerRef = this._viewContainerRef;
Copy link
Contributor Author

@fxck fxck Jan 30, 2017

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

@crisbeto
Copy link
Member

crisbeto commented Jan 31, 2017

@fxck regarding your comment about not being able to expose the escapePressed subject, I'm not sure that the DialogRef should be the one that knows about what DOM events are happening in the DialogContainer. In #2825 I changed the relationship from it being container->ref to ref-container, which means that you could keep the subject in the container and do a dialogRef.containerInstance.escapePressed.subscribe(). Note that the containerInstance is private in #2825, but that can be refactored if necessary.

@fxck
Copy link
Contributor Author

fxck commented Jan 31, 2017

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

@crisbeto
Copy link
Member

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.

@fxck
Copy link
Contributor Author

fxck commented Jan 31, 2017

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.

@crisbeto
Copy link
Member

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.

@fxck
Copy link
Contributor Author

fxck commented Jan 31, 2017

I guess I'll wait until that gets merged then.

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 6, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes PR author has agreed to Google's Contributor License Agreement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants