Auralinna.blogTero Auralinna's blog about intriguing world of web development. Tweaking pixels since the '90s.

I am an experienced web developer with an eye for a solid UI/UX design. I have specialized in front-end development, responsive web design, modern web frameworks, and Content Management Systems. I also have experience in mobile apps development and back-end coding with PHP, Node.js, and Java. So I am a Full Stackish web developer with a strong passion for a beautiful front-end.

CodePen embeds with Contentful and Angular

I use CodePens in my blog posts and recently I noticed that there is an issue which prevents displaying CodePen embeds correctly. I use Contentful to manage blog posts content. Contentful supports embedding content via Embedly.

But for some reason, Embedly fails to show my CodePens in some cases. I couldn't find the reason nor any way to load them. They just didn't load. So I decided to stop hitting my head against the wall and make an implementation without Embedly.

I still use the existing embed feature in Contentful to embed CodePens. This way I get a nice preview on Contentful editor. But on the client-side, I load CodePens differently. Also because I don’t load Embedly’s Platform.js anymore I get a minor performance boost by loading fewer scripts.

Loading the CodePens

Contentful embedding produces HTML like below. We will turn this into a CodePen embed.

<p>
  <a href="https://codepen.io/teroauralinna/pen/rZZGpe" class="embedly-card">
    Embedded content: https://codepen.io/teroauralinna/pen/rZZGpe
  </a>
</p>

Service for loading CodePens

This new service will handle the CodePen embed script loading on-demand and then it will load embedded pens.

Method initCodePens finds all the .embedly-card elements and inserts needed attributes.

After that loadCodePens will either insert CodePen embed script into the head or if the script is already loaded it will call the method to load CodePens.

code-pen-embed.service.ts

import { Injectable, Inject, PLATFORM_ID } from '@angular/core'
import { isPlatformBrowser } from '@angular/common'
import { environment } from 'environments/environment'

@Injectable()
export class CodePenEmbedService {

  private CODEPEN_REGEX: RegExp = new RegExp(`https://codepen.io/${environment.codePenSettings.user}/pen/([a-zA-Z0-9]+)`)
  private CODEPEN_SCRIPT_ID: string = 'codepen-script'
  private CODEPEN_SCRIPT_SRC: string = 'https://static.codepen.io/assets/embed/ei.js'

  constructor(@Inject(PLATFORM_ID) private platformId: Object) {
  }

  public init() {
    if (isPlatformBrowser(this.platformId)) {
      this.initCodePens()
      this.loadCodePens()
    }
  }

  private initCodePens() {
    const embeds = Array.from(document.querySelectorAll('.embedly-card'))
    embeds.forEach(embed => {
      const codePenUrl = embed.getAttribute('href')
      const match = codePenUrl.match(this.CODEPEN_REGEX)
      const codePenId = match ? match[1] : null
      if (codePenId) {
        const embedParent = embed.parentElement
        embedParent.classList.add('codepen')
        embedParent.setAttribute('data-height', '700')
        embedParent.setAttribute('data-theme-id', 'dark')
        embedParent.setAttribute('data-default-tab', 'result')
        embedParent.setAttribute('data-user', environment.codePenSettings.user)
        embedParent.setAttribute('data-slug-hash', codePenId)
        embedParent.setAttribute('data-preview', 'true')
      }
    })
  }

  private loadCodePens() {
    if (document.getElementById(this.CODEPEN_SCRIPT_ID)) {
      window.__CPEmbed('.codepen')
    } else {
      const script = document.createElement('script')
      script.id = this.CODEPEN_SCRIPT_ID
      script.src = this.CODEPEN_SCRIPT_SRC
      script.type = 'text/javascript'
      script.defer = true
      document.querySelector('head').appendChild(script)
    }
  }
}

Register the new service

app.module.ts

import { CodePenEmbedService } from '@services/code-pen-embed.service'

@NgModule({
  ...
  providers: [
    ...
    CodePenEmbedService
  ],
  ...
})

Add service to the component

Lifecycle method ngAfterViewChecked will be called multiple times so codePenNeedsInitialize variable must be set to true when CodePens need initialization.

import { CodePenEmbedService } from '@services/code-pen-embed.service'

export class ExampleComponent implements AfterViewChecked {

  private codePenNeedsInitialize: boolean = true

  constructor(
    private codePenEmbedService: CodePenEmbedService
  ) {}

  ngAfterViewChecked() {
    if (this.codePenNeedsInitialize) {
      this.codePenEmbedService.init()
      this.codePenNeedsInitialize = false
    }
  }
}