| Version | Date | Notes | By |
|---|---|---|---|
| 0.2 | 2017-07-18 | Added drafts implementation | FPA |
| 0.1 | 2017-02-17 | Initial release | JFM |
Note: this guide assumes you already know how to create basic forms
To implement a form as a modal you must create an index.html file for that form (create/index.js or edit/index.js) if you didn't already have them. The html file must have the following code:
<template>
<modal modal-data.bind="modalData" modal-id.bind="modalData.id" model.bind="model" parent-view-model.bind="self" action.call="submit()">
<modal-body slot="portlet-body">
<schema-form alert.bind="alert" form-id.bind="formId" schema.bind="schema" model.bind="model" containerless></schema-form>
</modal-body>
</modal>
</template>
Then the index.js file must have the following variables at the top:
@bindable
modalData = {
id: 'document-revision-creation-modal',
title: 'form.title.create_new_document_revision',
buttons: false,
};
Do not forget to import bindable from aurelia framework. If the index.js file has a getViewStrategy function, delete it.
The listing index must also be modified in order to be able to display the modal and pass data to it. An index.html must be created for it if not created already. A complete example of an index.html for a listing view model is provided bellow:
<template>
<datatable schema.bind="datatable" listing-id="listingId"></datatable>
<compose view-model.bind="self.objectModalViewModel" model.bind="self.objectModalModel"></compose>
</template>
Notice the compose tag, that is the tag that creates the modal in the listing view model (and it is the only tag you need to add if the index.html file is already present).
The parameter view-model.bind will point to a variable in the current view model that can either be null or a string with the path to the modal view model (the create/index.js or edit/index.js).
The parameter model.bind will point to a variable that is either null or a javascript variable (int, obj, array, etc ...) that you want to make available to the modal (for creates that is usually null or foreign ids required to create the object, for edits, the id of the object being edited is required).
The index.js (for the listing view model) will also need a couple of variables and some changes to the datatables declaration
// variables at the top of the class
@bindable
objectModalViewModel;
@bindable
objectModalModel;
/* ... */
// function for "clearing" the references to the modal
// usually placed after the datatables declaration
nullifyObjectModal()
{
return new Promise((resolve, reject) => {
this.objectModalViewModel = null;
this.objectModalModel = null;
resolve(true);
reject(new Error('Error'));
}
)
}
The datatables declaration has a parameter named edit that usually has a string with the route to the edit view model. It must be replaced with the following callback:
(row) => {
this.nullifyObjectModal()
.then(() => {
this.objectModalViewModel = 'modules/documents/document-revisions/edit/index';
this.objectModalModel = row.id;
});
},
The actions array in the datatables serves to define buttons that appear at the top of the table. This is where we will put the button that will open the create new object modal:
buttons: [
{
attributes: { 'data-toggle': 'model' },
label: 'form.button.create_new_revisions',
icon: 'icon-plus3',
className: 'btn bg-success',
action: () => {
this.nullifyObjectModal()
.then(() => {
this.objectModalViewModel = 'modules/documents/document-revisions/create/index';
this.objectModalModel = null; // value depends on wether you have any information needed to create the object
});
}
}
],
Within the create or edit index.js file, in the activate method, you will receive whatever value you have assigned to objectModalModel.
Finally, you must place the code bellow in the attached method of your create or edit index.js (place this after the super.attached(); line):
$('#' + this.modalData.id).modal();
For now there is only one drafts listing by module, it will be analysed if it justifies to have a listing for each "resource".
If there isn't already a drafts listing in the module you are developing, this is the steps to create one.
drafts in your module root folder.index.js inside the folder created in the previous step, with the following contentimport {bindable, inject} from "aurelia-framework";
import {AppContainer} from "wemake-services/app-container";
import {BaseViewModel} from "base-view-model";
import {DraftsRepository} from "wemake-modules/administration/drafts/services/repository";
import {ListDrafts as BaseListDrafts} from "wemake-modules/administration/drafts/index";
@inject(AppContainer, DraftsRepository)
export class ListDrafts extends BaseListDrafts {
listingId = '<put-your-listing-id-here>'; // example: documents-drafts-listing
moduleId = xxx; // TODO: temporary identified by module id, will be refactored to use module slug/acronym
}
As you can see, this view-model is extending an existing one from the administration module, this way there is only a few things your have to overide.
To implemented drafts, associated with a given resource, there are a few steps you have to follow:
DraftsRepository to your resource repository, in order to facilitate its use.The steps above will now be explained in detail. A draft related to the creation of a document will be used as example.
Add an optional draftId parameter to your resource creation route. Note that in this case it was added /:draftId? in the route attribute.
// src/common/configs/route-config.js
// ...
{
route: 'documents/documents/predefined-workflow/create/:draftId?',
name: 'documents.documents.predefined-workflow.create',
moduleId: './modules/documents/documents/create/predefined-workflow/index',
nav: false,
title: 'Create document with predefined workflow',
settings: {
module: 'documents',
breadcrumbs: [
{title: 'page.breadcrumbs.documents'},
{title: 'page.breadcrumbs.document'},
{title: 'page.breadcrumbs.document_registration'},
{title: 'page.breadcrumbs.new_record'}
]
}
},
// ...
DraftsRepository to your resource repository, in order to facilitate its use.Add the following four methods to your resource repository.js class.
// repository.js
export class DocumentsRepository {
// ...
/**
* Creates a new draft
*
* @param data
*
* @returns {*}
*/
createDraft(data) {
return this.draftsRepository.create(data);
}
/**
* Updates an existing draft
*
* @param id
* @param data
*
* @returns {*}
*/
updateDraft(id, data) {
return this.draftsRepository.update(id, data);
}
/**
* Finds an existing draft
*
* @returns {*}
*/
findDraft(id) {
return this.draftsRepository.find(id);
}
/**
* Destroys an existing draft
*
* @param id
*
* @returns {*}
*/
destroyDraft(id) {
return this.draftsRepository.destroy(id);
}
// ...
}
In this step you must add some necessary methods to your resource creation view-model, please note that some methods might have to be customized for your specific needs.
export class CreateDocument extends BaseFormViewModel {
// ...
/**
* Handles activate event
*/
activate(params) {
super.activate(params);
// ...
if (this.draftId = params.draftId) {
return this.repository
.findDraft(this.draftId)
.then((response) => this.loadDraftContent(response.form_content));
}
}
/**
* Handles canDeactivate event
*/
canDeactivate() {
let canDeactivate = true;
if (this.initialModel && this.model) {
canDeactivate = this.model.equals(this.initialModel)
}
if (!canDeactivate) {
let options = {
title: this.appContainer.i18n.tr('text.attention'),
text: this.appContainer.i18n.tr('form.message.want-leave-without-saving-changes'),
type: 'warning',
showCancelButton: true,
cancelButtonText: this.appContainer.i18n.tr('form.button.cancel'),
confirmButtonColor: '#FF7043',
confirmButtonText: this.appContainer.i18n.tr('form.button.ok'),
closeOnConfirm: true
};
return this.appContainer.swalService.customAlert(options, (confirmed) => confirmed);
}
return canDeactivate;
}
/**
* Generates draft model
*
* @returns {{}}
*/
generateDraftModel() {
return {
resource_slug: 'documents.documents',
listing_id: 'documents-documents-listing',
form_id: this.formId,
form_content: this.model,
route: this.appContainer.router.currentInstruction.config.name,
name: this.model.document_name,
name_field: 'form.field.document-name'
}
}
/**
* Saves draft
*/
saveDraft() {
// do something here, if necessary
let draftModel = this.generateDraftModel();
return typeof this.draftId === 'undefined' || this.draftId === null
? this.createDraft(draftModel)
: this.updateDraft(draftModel);
}
/**
* Creates a new draft
*/
createDraft(draftModel) {
this.repository
.createDraft(draftModel)
.then((response) => this.afterDraftSubmissionCallback(response));
}
/**
* Updates an existing draft
*/
updateDraft(draftModel) {
this.repository
.updateDraft(this.draftId, draftModel)
.then((response) => this.afterDraftSubmissionCallback(response));
}
/**
* Destroys an existing draft
*/
destroyDraft() {
if (typeof this.draftId !== 'undefined' && this.draftId !== null) {
// destroys draft and sets draftId to null
this.repository.destroyDraft(this.draftId).then((response) => this.draftId = null);
}
}
/**
* Loads draft content
*
* @param content
*
* @returns {Promise}
*/
loadDraftContent(content) {
return new Promise((resolve, reject) => {
this.model.assign(content);
this.initialModel.assign(content);
// do something here, if necessary
resolve(true);
reject(new Error('Error'));
});
}
/**
* Callback to execute after draft submission
*
* @param response
*/
afterDraftSubmissionCallback(response) {
if (!response.status) {
// creates a new alert message
this.alert = this.alertMessage(response.status, response.message, response.errors);
} else {
// sets draft id
this.draftId = response.model ? response.model.id : this.draftId;
// updates initial model with current model
this.initialModel.assign(this.model);
// shows success notice
this.appContainer.notifier.successNotice(response.message);
}
}
// ...
}
The last step is to add a button to the resource creation form-schema in order to save the draft. Please add the following code:
this.draftButton = {
type: 'button',
label: 'form.button.save-draft',
action: (event) => viewModel.saveDraft(),
attributes: {
class: 'btn btn-warning'
},
icon: {
attributes: {
class: 'icon-pencil6'
}
}
};
By now you shall have a fully functional draft implementation. Please note that the drafts will be object of refactoring in order to reduce duplication of code along all draft implementations.
Todo
Todo