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

CodePen embeds with Contentful and Angular

Published on June 12, 2019. Edited on June 14, 2019.

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
    }
  }
}