JavaScript

Polyfills

In Flynt, most polyfills are provided out of the box with the help of babel-preset-env and corejs. This means that you can use almost any modern JavaScript / EcmaScript feature and it will work in all our target browsers. This also means, that polyfills will only be included in the final code, if they are used in the project.

Besides including polyfills, babel is also used for compiling modern JavaScript syntax that cannot be polyfilled, into code that can run in out targeted browsers. While this is really handy, it can also lead to inefficient code output. In a future version of Flynt, we will deal with this better through outputting separate JavaScript bundles for different target browsers. Until then, there are a few things that should be avoided. Most notably using async / await. One helpful article what to best avoid is by WebReflection.

Libraries

Slider / Carousel

In most project we use slick slider. It is easy to set up an get going. Recently, however, there have been multiple minor issues with this library. On the one hand, new versions have introduced minor bugs, and in general, the execution time is not optimal because there is a lot of DOM manipulation going on when initializing a slider.

One alternative library to try out is swiper. When using this, extra attention should be put on cross browser compatibility. We have yet to gain some more insights to determine if swiper will be made the default for our projects.

Date Picker

In the past we have had some good result with air-datepicker. This library is not actively maintained, however.

There has been some testing with the more modern library flatpicker. For many use cases this should be the go to solution. However, the year selection is not customizable, which could be a no go for certain desings.

Lazy Loading Images

TODO: explain lazysizes

Custom Elements

We use WebReflection/document-register-element as a polyfill for custom elements.

Normally we extend the HTMLDivElement as most of the Flynt components are divs.

A minimal script of a component should contain the following code

class SomeComponent extends window.HTMLDivElement {
}

window.customElements.define('flynt-some-component', SomeComponent, { extends: 'div' })

There are a couple of caveats when compiling these ‘native’ Custom Element V1 classes to ES5 code and when using the polyfill. Most notably is a constructor issue where you are not able to use this in the constructor context.

In order to avoid this issue, the following code should be used to minimize non-standard code

class SomeComponent extends window.HTMLDivElement {
  constructor (...args) {
    const self = super(...args)
    self.init()
    return self
  }
  
  init () {
  }
}

window.customElements.define('flynt-some-component', SomeComponent, { extends: 'div' })

We do use jQuery in our component. In order to facilitate the usage of it on a custom element, we use the following helper.

import $ from 'jquery'

class SomeComponent extends window.HTMLDivElement {
  constructor (...args) {
    const self = super(...args)
    self.init()
    return self
  }
  
  init () {
    this.$ = $(this)
  }
}

window.customElements.define('flynt-some-component', SomeComponent, { extends: 'div' })

This allows as to use this.$ to access the jQuery wrapped element that this represents. this, when talking about a custom element, is actually the DOM node, that you modify with the class that you are defining in the JavaScript file.

Inside a custom element, we use event delegation for almost anything. With jQuery, it is really easy do to event delegation. This means that instead of attaching an event to a child of the custom element, it is attached to the custom element itself but only fires if the currentTarget matches a certain selector.

All events that are delegated from the custom element should be attached in the constructor, through the init function.

As a best practice, we try to avoid attaching JavaScript behavior to CSS classes, but instead rely on data attributes for that. Also we try to standardize binding functions to the current instance of a class through the bindFunctions method. This will ensure, that when using a instance method as a callback, that this will always be bound to the current instance.

import $ from 'jquery'

class SomeComponent extends window.HTMLDivElement {
  constructor (...args) {
    const self = super(...args)
    self.init()
    return self
  }
  
  init () {
    this.$ = $(this)
    this.bindFunctions()
    this.bindEvents()
  }
  
  bindFunctions () {
    this.onSomeAction = this.onSomeAction.bind(this)
  }
  
  bindEvents () {
    this.$.on('click', '[data-action="someAction"]', this.onSomeAction)
  }
  
  onSomeAction () {
    console.log('onSomeAction', this)
  }
}

window.customElements.define('flynt-some-component', SomeComponent, { extends: 'div' })

Everytime a DOM element is upgraded to a custom element, or a custom element is attached to the DOM, the connectedCallback will be called. After the constructor, this is the second lifecycle method of a custom element. The third lifecycle method is disconnectedCallback. There is also adoptedCallback but we never use that.

The constructor will be called when a custom element is created (const someComponent = document.createElement('flynt-some-component')). The connectedCallback will be called when it is added to the DOM (document.body.appendChild(someComponent)). The disconnectedCallback will be called when it is removed from the DOM (document.body.removeChild(someComponent)).

This makes connectedCallback the best place to attach or initialize behavior that is dependend on the DOM. Examples are attaching events on window or body, or initializing other (jQuery) plugins. The disconnectedCallback should be used to unattached these events, or destroy instances of the initiliazed plugins.

In order to cache selectors, we define instance variables as early as possible in a resolveElements method.

With these concepts, our example component might look like this:

import $ from 'jquery'

class SomeComponent extends window.HTMLDivElement {
  constructor (...args) {
    const self = super(...args)
    self.init()
    return self
  }
  
  init () {
    this.$ = $(this)
    this.resolveElements()
    this.bindFunctions()
    this.bindEvents()
  }
  
  resolveElements () {
    this.$slider = $('[data-model="slider"]', this)
    this.$window = $(window)
  }
  
  bindFunctions () {
    this.onSomeAction = this.onSomeAction.bind(this)
    this.onWindowClick = this.onWindowClick.bind(this)
  }
  
  bindEvents () {
    this.$.on('click', '[data-action="someAction"]', this.onSomeAction)
  }
  
  connectedCallback () {
    this.$slider.slick()
    this.$window.on('click', this.onWindowClick)
    
  }
  
  disconnectedCallback () {
    this.$slider.slick('unslick')
    this.$window.off('click', this.onWindowClick)
  }
  
  onSomeAction () {
    console.log('onSomeAction', this)
  }
  
  onWindowClick (e) {
    e.preventDefault()
    this.$slider.slick('slickNext')
  }
}

window.customElements.define('flynt-some-component', SomeComponent, { extends: 'div' })

For most flynt compontents the attributeChangedCallback of Custom Elements V1 does not matter. However, this can be used to trigger certain behavior if an HTML attribute of that component changes. We actually mostly use that functionality for nested custom elements that are not flynt components.

In more interactive JavaScript components there needs to be a concept of state. Also, many times data needs to be passed from PHP to a JavaScript component. In order to unify our code regarding these two concepts, we have introduced props and state, taken from many popular JS frameworks.

To pass data from PHP to JS, we make use of some twig helpers and also regular HTML elements. To get an overview, the entire code would look something like this:

functions.php

<?php
  
add_filter('Flynt/addComponentData?name=SomeComponent', function ($data) {
    $data['jsonData'] = [
        'slides' => $data['slides'],
    ];
    return $data;
});

index.twig

<div class="flyntComponent" is="flynt-some-component">
  <script type="application/json">{{ jsonData|json_encode }}</script>
  <div data-model="slider"></div>
  <button data-action="someAction">{{ label }}</button>
</div>

script.js

import $ from 'jquery'

class SomeComponent extends window.HTMLDivElement {
  constructor (...args) {
    const self = super(...args)
    self.init()
    return self
  }
  
  init () {
    this.$ = $(this)
    this.props = this.getInitialProps()
    this.resolveElements()
    this.bindFunctions()
    this.bindEvents()
  }
  
  getInitialProps () {
    let data = {}
    try {
      data = JSON.parse($('script[type="application/json"]', this).text())
    } catch (e) {}
    return {
      ...data,
      slideCount: (data.slides || []).length
    }
  }
  
  resolveElements () {
    this.$slider = $('[data-model="slider"]', this)
    this.$window = $(window)
  }
  
  bindFunctions () {
    this.onSomeAction = this.onSomeAction.bind(this)
    this.onWindowClick = this.onWindowClick.bind(this)
  }
  
  bindEvents () {
    this.$.on('click', '[data-action="someAction"]', this.onSomeAction)
  }
  
  connectedCallback () {
    this.$slider.slick()
    this.$window.on('click', this.onWindowClick)
    
  }
  
  disconnectedCallback () {
    this.$slider.slick('unslick')
    this.$window.off('click', this.onWindowClick)
  }
  
  onSomeAction () {
    console.log('onSomeAction', this)
  }
  
  onWindowClick (e) {
    e.preventDefault()
    this.$slider.slick('slickNext')
  }
}

window.customElements.define('flynt-some-component', SomeComponent, { extends: 'div' })

Props can be seen as configuration that comes from outside that will not be changed during the lifecycle of a custom element.

State, on the other side will surely be changed by the component during its lifetime. In our example, state could be the current slide index. The default way of implementing initial state and state change should be the following.

class SomeComponent extends window.HTMLDivElement {
  ...
  init () {
    this.$ = $(this)
    this.props = this.getInitialProps()
    this.state = this.getInitialState()
    this.resolveElements()
    this.bindFunctions()
    this.bindEvents()
  }
  
  getInitialState () {
    return {
      currentSlide: null
    }
  }
  
  connectedCallback () {
    this.$slider.slick({
      afterChange: (e, slick, currentSlide) => {
        this.setCurrentSlide(currentSlide)
      }
    })
    this.setCurrentSlide(0)
    this.$window.on('click', this.onWindowClick) 
  }
  
  setCurrentSlide (currentSlide) {
		this.state.currentSlide = currentSlide
    // OR
    this.state = {
      ...this.state,
      currentSlide
    }
  }
	...
}

When setting state, we always use a direct assignment (this.state.currentSlide = currentSlide) or the spread operator ({...this.state, currentSlide}).

For props we always define the ones we use in a method at the top of it. This helps us keeping an overview about what props are used, shortens the code, and reminds us, that these should not be modified.

Example:

class SomeComponent extends window.HTMLDivElement {
  ...
  someMethod () {
    const {
      numberOfSliders,
      initialSlide,
      slides
    } = this.props
    ...
  }
	...
}