DEV Community

야 오왕
야 오왕

Posted on

Simple E-mail Footer Generator in Angular 9 using Flotiq

Concept

I wanted to create a simple e-mail footer builder application with the usage of Flotiq Headless CMS.

Application is split into 3 parts:

  1. Modules - a list of available modules user can drag & drop to Workspace
  2. Workspace - a catalogue of selected modules user can configure or order in a preferred way.
  3. Preview - a preview of user work. It displays prepared HTML, that can be used as footer.

Modules

Modules (elements that are used to build footer) are stored in Flotiq as an MJML template along with its properties.

Modules list:

  • Spacer
  • Button
  • Text
  • Hero
  • Image
  • Divider
  • Social
  • Text + Logo - 2 columns
  • Text + Image - 2 columns
  • Raw
  • Text + Text - 2 columns

Workspace

Every selected module contains settings that are set as properties in Flotiq. User can reorder modules and configure them. For example:

  • Change content of the module
  • Change font size, colours, module align
  • Reverse column display (for 2 column modules)
  • Change image and logo
  • Insert target URL (for buttons, and social modules)

Preview

User can review its work in the preview section. Every change in a module configuration and drop of the module into the Workspace regenerates view. User can test mobile, and desktop resolutions, as well as download, prepared HTML that can be inserted as a footer in used mail client.

Application screen

Full application

Tech stack

Why Flotiq?

I wanted to simplify as much as possible in this project. By storing modules and its configurations in Flotiq, I don't have to implement Dynamic Component Loader logic and store all the template components in my project.

Also, I don't have to rebuild my application every time I add or update module, because its data is stored externally.

Flotiq is very flexible in this case and user friendly, so implementing this concept in their product was really easy and time-saving. The user interface is really comfy to work with, so getting on board was really fast.

Module body in Flotiq

In Flotiq CMS I have created Modules Content Type Definition, which contains:

  • template type: string - MJML template of component.
  • icons type:string - one or many, split by comma for more than one in row (ex. text,plus,text)
  • image type: relation(media) - can be displayed instead of icons
  • properties type:relation(properties) - component settings ex. font-size, align, background image etc.

Module Body in Flotiq

Properties

Properties describe details of the module. Single property consists:

  • Key type: string - variable used in template (example: {{ borderColor }})
  • Value tyle: string - default property value
  • InputType type: select - type of input. Available: text, text editor, color picker, align select, direction select.

Retrieving module data from Flotiq

I have created a service, which is responsible for getting module data from Flotiq:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class FlotiqService {

  constructor(private http: HttpClient) { }

  getModules() {
    return this.http.get(
      environment.flotiqApiUrl +
      '/api/v1/content/modules?limit=100&page=1&hydrate=1&auth_token=' +
      environment.flotiqApiKey
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

So now, in the modules.component.ts file I can retrieve them:

[...imports...]
export class ModulesComponent implements OnInit {

  modules: Module[];
  pending = true;

  constructor(private flotiqService: FlotiqService) { }

  ngOnInit() {
    this.flotiqService.getModules()
    .subscribe((data: Response) => {
      this.modules = data.data;
      this.pending = false;
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

and display:

  <app-module class="rounded overflow-hidden shadow-lg bg-white cursor-move"
    cdkDrag
    *ngFor="let item of modules" [module]="item">
  </app-module>
Enter fullscreen mode Exit fullscreen mode

Managing Drag&Drop functionality between components

Everything is split into components, so for drag & drop functionality to work correctly, the connector service is required:

[...imports...]

@Injectable({
  providedIn: 'root'
})
export class BuilderService {

  htmlChanged = new Subject<SafeHtml>();

  drop(event: CdkDragDrop<string[]>) {
    if (event.previousContainer === event.container) {
      moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
    } else {
      copyArrayItem(cloneDeep(event.previousContainer.data),
        event.container.data,
        event.previousIndex,
        event.currentIndex);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This changes the way we connect D&D lists. We omit [] brackets in cdkDropListConnectedTo property. We pass a string value now, which is the id of the list in another component

cdkDropListConnectedTo must have the same value as cdkDropList element id in another component. Look at the code fragments below as a reference:

Part of modules.component.html file:

<div class="grid grid-cols-1 gap-6"
  cdkDropList
  #availableList="cdkDropList"
  [cdkDropListData]="modules"
  cdkDropListConnectedTo="selectedList"
  [cdkDropListSortingDisabled]="true">
  <div *ngIf="pending"
    class="block hover:bg-gray-50 focus:outline-none focus:bg-gray-50 transition duration-150 ease-in-out">
      Loading...
  </div>
  <app-module class="rounded overflow-hidden shadow-lg bg-white cursor-move"
    cdkDrag
    *ngFor="let item of modules" [module]="item">
  </app-module>
</div>
Enter fullscreen mode Exit fullscreen mode

Part of workspace.component.html file:

<div
  class="bg-white relative workspace"
  cdkDropList
  id="selectedList"
  [ngClass]="{'workspace-empty': !selectedModules.length}"
  [cdkDropListData]="selectedModules"
  (cdkDropListDropped)="drop($event)">
    .....
Enter fullscreen mode Exit fullscreen mode

Module settings in the Workspace section

Module Settings

The user can configure specific module settings like content, colour, align, line height etc. Every module settings save, will trigger a refresh in the preview section.

Fragment of settings.component.html file:

[....]
      <div class="w-8/12 mt-1 relative rounded-md shadow-sm">
        <input
          *ngIf="property.inputType === 'text'"
          class="form-input block w-full sm:text-sm sm:leading-5"
          type="text"
          placeholder=""
          [(ngModel)]="property.value"
          name="{{ property.key}}">
        <ckeditor
          *ngIf="property.inputType === 'text-editor'"
          [editor]="editor"
          [data]="property.value"
          [(ngModel)]="property.value"
          [config]="editorConfig">
        </ckeditor>
[....]
Enter fullscreen mode Exit fullscreen mode

Compiling templates with Handlebars

Before sending prepared MJML template to its API, it has to be compiled by Handlebars. Every variable enclosed in {{ }} brackets is replaced by the value set in the module settings.

This function takes two parameters:

  • template (MJML Template)
  • context (module properties values)

In the first step, the MJML template is prepared by using Handlebars compile function. It returns a function that requires module properties values to return a fully compiled template.

Module properties values are passed to a temporary array and then passed to compiledTemplate function that is returned.

  /**
   * Handlebars template compiler
   */
compile(template: string, context: Property[]): string {
    const compiledTemplate = Handlebars.compile(template, {noEscape: true});
    const parameters = [];
    context.forEach((element: Property) => {
      parameters[element.key] = element.value;
    });

    return compiledTemplate(parameters);
}
Enter fullscreen mode Exit fullscreen mode

Retrieving HTML from MJML API

When the module is added, or its settings are changed, the request is sent to MJML API to generate fresh HTML. This is what function refresh does. Firstly, it generates compiled MJML template - generateMjml. Generated MJML is passed to mjmlService to retrieve HTML file readable for mail clients.

refresh(selectedModules: Module[]) {
    const mjml = this.generateMjml(selectedModules);
    return this.mjmlService.render(mjml);
}
Enter fullscreen mode Exit fullscreen mode

generateMjml function in preview.service.ts file:

generateMjml(selectedModules: Module[]) {
    let tmpMjml = '<mjml>' +
      '<mj-body>';
    selectedModules.forEach(module => {
      tmpMjml = tmpMjml + this.compile(module.template, module.properties);
    });

    tmpMjml = tmpMjml +
      '</mj-body>' +
      '</mjml>';

    return tmpMjml;
}
Enter fullscreen mode Exit fullscreen mode

Body of mjml.service.ts file:

[...imports...]

@Injectable({
  providedIn: 'root'
})
export class MjmlService {

  constructor(private http: HttpClient) { }

  render(mjml) {
    const httpOptions = {
      headers: new HttpHeaders({
        'Content-Type':  'application/json',
        'Authorization': 'Basic ' + btoa(environment.mjmlApplicationKey + ':' + environment.mjmlPublicKey)
      })
    };
    return this.http.post(environment.mjmlApi + '/v1/render', {mjml}, httpOptions);
  }
}
Enter fullscreen mode Exit fullscreen mode

Preview Section & SafePipe

This section displays the current work of the user. As mentioned earlier, every change in the Workspace regenerates footer template. Generated HTML is bound to the srcdoc iframe property.

Part of preview.component.html:

<iframe #preview class="preview"
        [ngStyle]="{'max-width': previewMaxWidth ? previewMaxWidth+'px' : '100%'}"
        [srcdoc]="html| safe: 'html'"></iframe>
Enter fullscreen mode Exit fullscreen mode

Angular does not allow rendering HTML code after compilation by default. It can be omitted by implementing SafePipe. It tells Angular whatever we want to display is safe and trusted.

@Pipe({
  name: 'safe'
})
export class SafePipe implements PipeTransform {

  constructor(protected sanitizer: DomSanitizer) {
  }
  transform(value: any, type: string): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl {
    switch (type) {
      case 'html': return this.sanitizer.bypassSecurityTrustHtml(value);
      case 'style': return this.sanitizer.bypassSecurityTrustStyle(value);
      case 'script': return this.sanitizer.bypassSecurityTrustScript(value);
      case 'url': return this.sanitizer.bypassSecurityTrustUrl(value);
      case 'resourceUrl': return this.sanitizer.bypassSecurityTrustResourceUrl(value);
      default: throw new Error(`Invalid safe type specified: ${type}`);
    }
  }

}
Enter fullscreen mode Exit fullscreen mode

Final Preview

Simple footer built with this application:

Final Preview

Summary

Connecting Angular application with Flotiq Headless CMS was really nice. Their documentation was clear and made no problems with implementing my idea of simple footer builder. They have a self-explanatory onboarding process, so it just took a little time to create object schema there, and I began transforming my visions into code. Cheers!

Resources

  1. Flotiq Main Page
  2. Project Github Repo
  3. Flotiq docs

Top comments (2)

Collapse
 
andrzejwp profile image
andrzejwp

Interesting stuff. Why did you choose MJML?

Collapse
 
gyrosh profile image
야 오왕

Hi, thanks for comment. I choose MJML, because it is component based framework, that can create HTML templates in a way mail clients have no problem with rendering.