Developer Guide

Before diving into the developer guide, please make sure you have gone thru the Overview and User Guide sections. 

Assuming you have done that, the developer guide will focus on the various methodologies for extending the SonicJs core including theming, module development, overriding core functions, advanced configuration, integration, etc.

Hooks

Hooks are what gives SonicJs it's flexibility.  It can be a bit confusing at first if you haven't worked with the event-emitter design pattern. However, once you get used to it, you'll see how well it enables us to follow the SOLID principals

Hooks allow us to alter core functionality and even the functionality defined in other modules without touching the code within the target function.

Let's look at a simple example NOT using hooks:

// Get an array of objects of a certain content type,
// and optionally pass in a filter to fine tune the results
function getDataForMyModule(contentType, filter){
  let data = getDataFromDatabase(contentType, filter);
  return;
}
// Example call to the above function
// We want all records of type "blog"
let data = getDataForMyModule("blog", {});

Simple enough right? We want a list of content types of a certain kind ("blog" in our example). 

But what if we wanted to implement a site-wide rule dictating that we never wanted to display a blog post with a future publish date. This will allow us to load up our website with future blog post that will automatically publish once their publish date has been reached.

// Here, we had added our new business rule directly in the function.
function getDataForMyModule(contentType, filter){
  filter.publishDate < new Date(); // <--here is the new filter we're adding
  let data = getDataFromDatabase(contentType, filter);
  return;
}

But there is a MUCH better way of adding this new business rule. Take a look at a different approach:

// Get an array of objects of a certain content type,
// and optionally pass in a filter to fine tune the results
function getDataForMyModule(contentType, filter){
  eventBusService.emit("preDataFetch", filter);
  let data = getDataFromDatabase(contentType, filter);
  return;
}
// Before calling the above function, we'll setup our business rule
eventBusService.on('preDataFetch', function (filter) {
 filter.publishDate < new Date();
});
// Example call to the above function
// We want all records of type "blog"
let data = getDataForMyModule("blog", {});

So we're doing the same thing as before, but we've  broken away from the need to ever again have modify our core getDataForMyModule() function.

We can now very easily implement the "S" and "O" in SOLID - Each time we want to implement a business rule in our getDataForMyModule(), we can simply "subscribe" to the "emitter event" and inject our new code.

Hook List

Below is a list of the core system hooks that can be overwritten in your module development. You can also define your own hooks in custom modules. If you find yourself needing a hook in the core system that doesn't exist, please don't hesitate to raise an issue on Github Issues.

Async

Emitters can be subscribed to both sync and async depending on your use case. In most cases you will want to use await, unless you are not concerned about other code running in parallel. A good example might be tracing or analytics. If unsure, its best to use await.

Don't be overwhelmed

Don't be overwhelmed by the large list of Hooks. Most of them are in place to support the inner workings of the CMS. For the vast majority of custom module development tasks, you only need to work with a handful of them.

  • High Level
    • startup
    • started
  • Events
    • postBegin
    • requestBegin
    • afterFormSubmit
    • firstPageLoaded
  • Page Generation
    • getRenderedPagePostDataFetch
    • preRender
    • preRenderTemplate
    • getRenderedPagePostDataFetch
    • preProcessSections
    • preProcessRows
    • preProcessColumns
    • postPageDataFetch
    • postPageRender
    • postProcessPage
  • Content
    • contentUpdated
    • contentCreated
    • contentCreatedOrUpdated
  • ShortCodes
    • preProcessModuleShortCode
    • beginProcessModuleShortCode
    • beginProcessModuleShortCodeDelayed
  • Urls
    • processUrl
    • preProcessPageUrlLookup
  • Modules
    • modulesLoaded
    • postModuleGetData
    • postProcessModuleShortCodeProccessedHtml
  • Admin
    • postAdminPageRender
    •  

If you want to see SonicJs hooks in the source code simply do a full text search (CMD-SHIFT-F in VS Code) and search for "emitterService.emit(". This will allow you to see where the hooks are defined in the core source code.

Using Hooks

To use a hook, we add it to the startup function of a module's service. The example below is from the alert module located here:
[sonicjs root]/server/modules/alert/services/alert-main-service.js

var dataService = require('../../../services/data.service');
var emitterService = require('../../../services/emitter.service');
var globalService = require('../../../services/global.service');

module.exports = alertMainService = {
    startup: async function () {
        emitterService.on('beginProcessModuleShortCode', async function (options) {
            if (options.shortcode.name === 'ALERT') {
                options.moduleName = 'alert';
                await moduleService.processModuleInColumn(options);
            }
        });
    },
}

In the above example, we are subscribing to the beginProcessModuleShortCode hook which is described in more details below.

Common Hooks

Below are the common hooks that you may need to use for module development.

startup

Emitted when the system's services are loaded. This is the first hook called when the system is starting up. In the below example, the module service uses this hook to know when it should start to load the modules. It is rare that a non-core developer would need to use this hook, however we included it in the common list since its the first hook.

    emitterService.on("startup", async function ({app}) {
      await moduleService.processModules(app);
    });

beginProcessModuleShortCode

Emitted when the SonicJs core engine is processing shortcodes. This is the most common hook used in SonicJs for module development.

        emitterService.on('beginProcessModuleShortCode', async function (options) {
            if (options.shortcode.name === 'ALERT') {
                options.moduleName = 'alert';
                await moduleService.processModuleInColumn(options);
            }
        });

postModuleGetData

Anytime you want to pull in additional data, you will use the postModuleGetData hook. The blog module is a good example of this as we want to pull in a list of blog posts and display them in our view. Here is an example:

    emitterService.on("postModuleGetData", async function (options) {
      if (options.shortcode.name === "BLOG") {
        await blogMainService.getBlogData(options);
      }
    });

So after (hence to prefix of "post") we get a default data for the module, which includes anything that we entered in the settings form when we first drag and dropped the module onto a page, we then want to get a list of blog posts. For reference the getBlogData(options) function get's a list of blog posts - it gets all of them by default or just the one's with a certain tag if the user is on a tag listing page. You can also review the entire blog module service here: server/modules/blog/services/blog-main-service.js

addUrl

This hook allows us to programmatically add urls that can be rendered by the CMS. This could be regular pages, blog posts, taxonomy pages (like for showing content with a certain tag) or any custom content type that you create. The below example is again from the blog module:

    emitterService.on("addUrls", async function (options) {
      var blogs = await dalService.contentGet(
        null,
        "blog",
        null,
        null,
        null,
        null,
        null,
        null,
        true
      );
      blogs = blogs.sort((a, b) => (a.createdOn > b.createdOn ? 1 : -1));

      for (let index = 0; index < blogs.length; index++) {
        const blogPrevious = index > 0 ? blogs[index -1] : {};
        const blog = blogs[index];
        const blogNext = index < (blogs.length -1) ? blogs[index +1] : {};
        urlService.addUrl(blog.url, "blogHandler", "exact", blogData.title, blog.id, blogPrevious?.url, blogNext.url);
      }
    });
  },

processUrl

This hook allows us to listen for incoming urls and respond accordingly. In the below example, the blog details module waits to process the Urls previously setup via the addUrl hook:

    emitterService.on("processUrl", async function (options) {
      if (options.urlKey?.handler === "blogHandler") {
        const blogPost = await dataService.getContentByUrl(options.urlKey.url);

        let blogDetailsUrl = "/blog-details";
        options.req.url = blogDetailsUrl;
        var { page: pageData } = await contentService.getRenderedPage(
          options.req
        );
        options.page = pageData;

        //overrides
        options.page.data.heroTitle = blogPost.data.title;

        return;
      }
    });

In the above code, we're telling the CMS that anytime you process a url that is supposed to be handled by the blogHandler, use the page that can be found at the /blog-details url. This means that we have the option of managing our blog details page for our blog with a regular CMS page, so we get all the benefits of being able to manage it with the drag and drop visual page editor. We can also use a page template in this case.

contentCreatedOrUpdated

This hook will be executed anytime content is created or updated. For example, anytime we add or update a page on our site, we want to make sure that the CMS knows about the new or updated url for that page. The below sample is from the page module:

    emitterService.on("contentCreatedOrUpdated", async function (options) {
      if (options.contentTypeId === "page") {
        const pageData = JSON.parse(options.data);
        urlService.addUrl(pageData.url, "pageHandler", "exact", pageData.title, options.id);
      }
    });

afterFormSubmit

This hook allows us to process data submitted thru user-facing forms such as a newsletter signup or a contact form. The below is a simplified version of the newsletter signup:

    emitterService.on("afterFormSubmit", async function (options) {
      if (options.data.contentType !== "newsletter") {
        return;
      }

      let encryptedEmail = cryptoService.encrypt(options.email);

      let payload = {
        data: {
          contentType: options.contentType,
          email: encryptedEmail,
        },
      };

      // save the form
      await dataService.contentCreate(payload);
    });

More Hooks?

As mentioned above, there are quite a few additional hooks, but they are currently used only internally by the CMS. 

Working with hooks takes a bit of practice, but once you get used to them, you'll see just how powerful and time-saving working with SonicJs can be.