7 Patterns to launch Polymer dialogs
All patterns we used so far to launch dialogs with Polymer. Winner: Use mixins to reuse dialog business logic and instances.
Pattern #1: Declarative dialogs
Many Polymer starter examples declare dialogs in the element’s template:
<my-dialog id="dialog"></my-dialog>
<paper-button on-tap="open">Open</paper-button...open() {
this.$.dialog.open();
}
The downside is that the dialog template gets always stamped on startup, independently if the user ever opens the dialog or not.
Pattern #2: Dynamic dialog creation
Creating the dialog when the user clicks the button avoids this issue:
<paper-button on-tap="open">Open</paper-button...open() {
const dialog = new MyDialog();
Polymer.dom(document.body).appendChild(dialog);
// Wait until the dialog is added to the DOM
setTimeout(() => this.$.dialog.open(), 1);
}
Explanation: We add the dialog to the document body instead of the current element to prevent layering issues (seeing your dialog underneath your app). Be aware that the launching element won’t receive any events fired by the dialog because the dialog isn’t a child of it any longer.
The main issue with this approach is that we create now a new dialog whenever the user clicks the button.
Pattern #3: Lazy created singleton
We can avoid the multiple instances by lazily creating a singleton:
<paper-button on-tap="open">Open</paper-button...open() {
this.getDialog().then(dialog => dialog.open());
}getDialog() {
if (this.dialog) {
return Promise.resolve(this.dialog);
} this.dialog = new MyDialog();
Polymer.dom(document.body).appendChild(this.dialog);
return new Promise(resolve => {
setTimeout(() => resolve(this.dialog), 1);
};
}
Explanation: We encapsulate the lazy creation in an asynchronize method to get an easy-to-use Promise-based API.
This is our favorite pattern if we want to launch a dialog from exactly one element. Things get more tricky if you have to launch the dialog from multiple elements because you would have to copy & paste the above lines over and over.
Pattern #4: Share launching code via mixins
Mixins are a great way to share the launching code:
MyDialogMixin = parent => class MyDialogMixin extends parent {
openMyDialog() {
this.getMyDialogInstance().then(dialog => dialog.open());
} getMyDialogInstance() {
if (MyDialogMixin.dialog) {
return Promise.resolve(MyDialogMixin.dialog);
} MyDialogMixin.dialog = new MyDialog();
Polymer.dom(document.body).appendChild(MyDialogMixin.dialog);
return new Promise(resolve => {
setTimeout(() => resolve(MyDialogMixin.dialog), 1);
};
}
}
Each element can now easily launch dialogs via the mixin method:
<paper-button on-tap="open">Open</paper-button...class MyElement extends MyDialogMixin(Polymer.Element) {
open() {
this.openMyDialog();
} ...
Important: Ensure to include the name of the dialog in the openXXX()
and in getXXXInstance()
method. This avoids name clashes when adding multiple dialog mixins to an element.
Pattern #5: Move business logic into the mixin
We found the mixins the perfect place for all business logic related to the dialog: prepare data for the dialog, save data from the dialog, etc.
MyDialogMixin = parent => class MyDialogMixin extends parent {
openMyDialog(itemId) {
this.getMyDialogInstance().then(dialog => {
// Load data in dialog before opening
dialog.item = ReduxStore.getState().itemsById[itemId];
dialog.open();
});
} getMyDialogInstance() {
if (MyDialogMixin.dialog) {
return Promise.resolve(MyDialogMixin.dialog);
} MyDialogMixin.dialog = new MyDialog();
// Update state if the user clicks [save] button in dialog
MyDialogMixin.dialog.addEventListener('save', e => {
const {update} = e.detail;
const action = {
type: 'item/updated',
update,
};
ReduxStore.dispatch(action);
});
Polymer.dom(document.body).appendChild(MyDialogMixin.dialog);
return new Promise(resolve => {
setTimeout(() => resolve(MyDialogMixin.dialog), 1);
};
}
}
Pattern #6: Event consumption via mixins
As mentioned above, adding the dialog to the document.body
prevents the launching element from receiving events fired by the dialog. Mixins allow us to bring this back as well:
MyDialogMixin = parent => class MyDialogMixin extends parent {
... getMyDialogInstance() {
... MyDialogMixin.dialog.addEventListener('save', e => {
const {update} = e.detail;
this.dispatchEvent(new CustomEvent('item-saved', {
details: {update},
});
}); ...
}
}
Now you can consume these events from the launching element, e.g.:
ready() {
this.addEventListener('item-save', e => console.log('Saved!'));
}
Before using this, you need to carefully think through what’s going to happen when the event is fired: Our dialog is a singleton. This means that the event listener will be called independently of which element launched the dialog. Sometimes this is what you want. Often it isn’t.
Pattern #7: Callbacks for launcher specific events
To avoid this, just pass the callback to the open method in the mixin:
MyDialogMixin = parent => class MyDialogMixin extends parent {
openMyDialog(callback) {
this.getMyDialogInstance().then(dialog => {
MyDialogMixin.callback = callback;
dialog.open();
});
} getMyDialogInstance() {
if (MyDialogMixin.dialog) {
return Promise.resolve(MyDialogMixin.dialog);
}MyDialogMixin.dialog = new MyDialog();
MyDialogMixin.dialog.addEventListener('save', e => {
const {update} = e.detail;
MyDialogMixin.callback(update);
});
Polymer.dom(document.body).appendChild(MyDialogMixin.dialog);
return new Promise(resolve => {
setTimeout(() => resolve(MyDialogMixin.dialog), 1);
};
}
}
Explanation: We leverage the fact that the dialog can only be once on the screen (otherwise we couldn’t use a singleton for the dialog element itself).
Now, the launching element can provide a callback that is called only when the dialog was opened by the element itself:
class MyElement extends MyDialogMixin(Polymer.Element) {
open() {
this.openMyDialog(e => console.log('Saved'));
}...
Happy coding!
Photo: Marcu Ioachim