Skip to content

Instantly share code, notes, and snippets.

@totten
Last active June 11, 2018 23:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save totten/cecd92ce13686888e729cc857989358a to your computer and use it in GitHub Desktop.
Save totten/cecd92ce13686888e729cc857989358a to your computer and use it in GitHub Desktop.

Basic design:

  1. Each component in the palette ("General Info", "Addresses", "Profile 123") corresponds to an AngularJS directive. It accepts arguments (like {contactId: param.cid}).
  2. Each custom form (eg "View Individual" page) is effectively an AngularJS HTML document.

This design means:

  1. We're not responsible for defining all the edge-cases/nuances of how components interact with each other at runtime. This interaction is dictated by upstream's model.
  2. The structure of the user's configured form matches the structure of an Angular form in developer docs. When looking at a page, you can drill-down and see a representation of the form which looks the same as the representations you would have in upstream docs or in your ang/ folder.
  3. The HTML/JS which defines the form can be bundled and cached. As a user navigates among different contact records, the only thing that needs to change is the ID/data.

There is one key issue not addressed by AngularJS (wherein we need to fill the gap): the palette. AngularJS allows you declare reusable directives (e.g. crm-ui-tabset or crm-contact-addresses), but it doesn't provide rich introspection/reflection abilities for browsing available directives. To provide a palette, we need our own listing of acceptable directives.

There is another practical issue -- when implementing the editor (where an admin can drag/drop elements of the palette), you'll need to read/write some data-format. The format for using AngularJS directives is HTML (e.g. <div crm-contact-general="{contactId: param.cid}"></div>), but you may not be comfortable reading/writing this format. If it's easier, we can use an equivalent JSON data-strcuture. (There are two examples of a custom form described below -- one in canonical HTML, one in equivalent JSON.)

<?php
// The server side is aware of a library of components that can be mixed/matched (i.e. the palette).
// This info could equivalently be moved into `*.json` or `*.php` files that are scanned, but this is
// fine for a quick-and-dirty start. Once we have the hook, it's easy for downstream tooling (eg civix)
// to bridge into other formats.
function findDirectives() {
$cached = Civi::cache()->get('directives');
if ($cached !== NULL && !\CRM_Core_Config::singleton()->debug) return $cached;
$items['crm-contact-general'] = [
'title' => ts('General Info'),
'template' => [...'contactId' = > 'param.cid'...]
];
$items['crm-contact-addresses'] = [
'title' => ts('Addresses'),
'template' => [...'contactId' = > 'param.cid'...]
];
$items['crm-contact-emails'] = [
'title' => ts('emails'),
'template' => [...'contactId' = > 'param.cid'...]
];
$items['crm-contact-misc'] = [
'title' => ts('Miscellaneous'),
'template' => [...'contactId' = > 'param.cid'...]
];
$items['crm-contact-profile'] = [
'title' => ts('Miscellaneous'),
'template' => [...'profileId' = > 'param.cid', 'profileId' => ...]
];
CRM_Utils_Hook::alterDirectives($items);
Civi::cache()->set('directives', $items);
return $items;
}
<!--
Based on the available palette items and user preferences, we can generate markup like this.
-->
<div crm-layout="{type: 2col}">
<div crm-col="left">
<div crm-contact-general="{contactId: param.cid}"></div>
<div crm-contact-addresses="{contactId: param.cid}"></div>
</div>
<div crm-col="right">
<div crm-contact-misc="{contactId: param.cid}"></div>
<div crm-contact-emails="{contactId: param.cid}"></div>
<div crm-contact-profile="{contactId: param.cid, profileId: 123}"></div>
</div>
</div>
/**
* The `example-rendered.html` can work as a canonical file-storage format, but I understand
* if doing that feels scary (e.g. too much DOM). It's fine to store in a JSON format -- as long as it
* has a very simplistic mapping to HTML.
*
* Ex: These lines would be equivalent:
* HTML: <div crm-layout="{type: 2col}"></div>
* JSON: {"#type": "div", "crm-layout": {"type": "2col"}}
*/
{
"crm-layout": {"type": "2col"},
"#type": "div",
"#children": [
{
"crm-col": "left",
"#type": "div",
"#children": [
{"#type": "div", "crm-contact-general": {"contactId": "param.cid"}},
{"#type": "div", "crm-contact-addresses": {"contactId": "param.cid"}}
]
},
{
"crm-col": "right",
"#type": "div",
"#children": [
{"#type": "div", "crm-contact-misc": {"contactId": "param.cid"}}
{"#type": "div", "crm-contact-emails": {"contactId": "param.cid"}}
{"#type": "div", "crm-contact-profile": {"contactId": "param.cid", "profileId": 123}}
]
}
]
}
// The on-disk file-format should use symbolic directive names (e.g. "crm-contact-addresses"
// instead of "crm-legacy-contact with block=addresses").
// Of course, most of the directives already exist as AJAX helpers and they use the same
// calling convention, so they should be able to share the implementation.
angular.module('crmContact').directive('crmContactGeneral', function(crmContactCreateLegacyWrapper) {
return crmContactCreateLegacyWrapper(...);
});
angular.module('crmContact').directive('crmContactAddresses', function(crmContactCreateLegacyWrapper) {
return crmContactCreateLegacyWrapper(...);
});
angular.module('crmContact').directive('crmContactMisc', function(crmContactCreateLegacyWrapper) {
return crmContactCreateLegacyWrapper(...);
});
angular.module('crmContact').service('crmContactCreateLegacyWrapper', function($http, ...) {
// Use AJAX to render the blocks - same as we do on current dashboard
return function crmContactCreateLegacyWrapper() {
// ...
};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment