Simple E-mail Footer Generator in Angular 9 using Flotiq
Tags: angular, flotiq, typescript, cms
Published: 2020-07-30 08:12:54

## Concept

I wanted to create a simple e-mail footer builder application with the usage of [Flotiq Headless CMS](https://flotiq.com). 

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](https://dev-to-uploads.s3.amazonaws.com/i/h3iloxxykhuaoy5wlpbv.png)


## Tech stack

* [Angular 9](https://angular.io/)
* [Angular Material](https://material.angular.io/) - icons, drag & drop
* [Tailwind CSS](https://tailwindcss.com/) - visual styling
* [Handlebars](https://handlebarsjs.com/) - template compiling before sending to MJML API
* [JSZip](https://github.com/Stuk/jszip) - generated footer download

## 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](https://dev-to-uploads.s3.amazonaws.com/i/uuioaveqwq5mc3h0hezk.png)

### 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:

```typescript
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
    );
  }
}
```

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

```typescript
[...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;
    });
  }
}
```

and display:

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

## Managing Drag&Drop functionality between components

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

```typescript
[...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);
    }
  }
}
```

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:

```html
<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>
```

Part of `workspace.component.html` file:

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

## Module settings in the Workspace section

![Module Settings](https://dev-to-uploads.s3.amazonaws.com/i/elenao0s4b4uqsdnwfzv.png)
    
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:

```html
[....]
      <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>
[....]
```

## 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.

```typescript
  /**
   * 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);
}
```

## 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.

```typescript
refresh(selectedModules: Module[]) {
    const mjml = this.generateMjml(selectedModules);
    return this.mjmlService.render(mjml);
}
```

`generateMjml` function in `preview.service.ts` file:

```typescript
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;
}
```

Body of `mjml.service.ts` file:

```typescript
[...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);
  }
}
```



## 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`:

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

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.

```typescript
@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}`);
    }
  }

}
```

## Final Preview

Simple footer built with this application:

![Final Preview](https://dev-to-uploads.s3.amazonaws.com/i/xgex9nn1q2icgjl32muy.png)


## 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](https://flotiq.com)
2. [Project Github Repo](https://github.com/Gyrosh/footer-builder)
3. [Flotiq docs](https://flotiq.com/docs)