Creating new EPT modules

05/03/2024, by Ivan

Easiest way to create a new EPT module is Drush command (for Drush 12+). To use this command, you need to enable EPT Core Starterkit module:

EPT Starterkit

After that generator EPT modules will be available:

drush generate ept:module

Start machine name with ept_* prefix, it's required for working all EPT modules.

EPT module generator

You can also use EPT Starterkit in EPT Core modules folder. Just rename all ePt_starterkit in files with machine name of your new EPT module.

https://www.drupal.org/project/ept_core

Or copy EPT Text module and replace machine name there.

https://www.drupal.org/project/ept_text

Because, it's simplest EPT module and it contains all basic EPT settings. 

Creating EPT module Step by Step

(Some screenshots can be for EBT Countdown module)

Copy existing EPT Text or EPT kickstarter module as boilerplate or use drush generate ept:module command. 

 EPT Text module contains next folders:
/ept_text/config/install - with configs for EPT Text paragraph type and field instances. Other EPT modules can contain configs for paragraph types and field storages.
/ept_text/templates - here is paragraph--ept-text--default.html.twig template for the paragraph.
/ept_text/tests - here are tests for EPT modules, now only one test for installation module.

And other regular drupal module files: composer.json, ept_text.info.yml, readme.md. You can read more about creating custom Drupal modules in official documentation:

https://www.drupal.org/docs/develop/creating-modules

I will create a new module EPT Countdown which will use this javascript plugin FlipDown:

https://github.com/PButcher/flipdown

FlipCount.js

Fork Github repository and submit it to Packagist

All 3rd party libraries should be forked and placed on Packagist, for example:

https://packagist.org/packages/levmyshkin/flexslider

from:

https://github.com/levmyshkin/flexslider

Then Composer can download them as usual packagist libraries. These 3rd party libraries must have "type": "drupal-library", and they will be download in /libraries folder, by default:
https://github.com/levmyshkin/flexslider/blob/master/composer.json

EBT library

Let's fork github repository for FlipDown.

It might be unusual to fork git repositories, instead of using source git repositories. But I believe it will be easier to use EPT modules without manipulations with main composer.json file to add external links to repositories in it. Imagine, how it's hard for newbies to install composer, update composer.json file manually and place repository source in right place. With own packagist library the process of installation is getting simple. So let's keep all 3rd party libraries on packagist.

 

fork git repository

On forking git repo page you can rename repository. You must keep it clean, without Capital letters and special chars, dash - and underscore _ are working fine.

Rename repository

Now we have a new git repo:

https://github.com/levmyshkin/flipdown

Then we need to add composer.json file in this repository with "type": "drupal-library":

git add composer.json
git commit -m 'Add Composer.json file'
git push origin master

Here is composer.json file:

https://github.com/levmyshkin/flipdown/blob/master/composer.json

If you check your current tags in new git repo, it will be empty:

git tag

Git tags

Usually I follow version from source repo, for example if  latest version of library was 1.4.6, I just increment minor version to 1.4.7. FlipDown repo didn't have any tags or releases, so I created 1.0.0 version for new repo:

git tag 1.0.0
git push origin 1.0.0

We need new tag, because it contains our new composer.json with "type": "library"

Why we can't simply copy javascript library inside module?

We can copy only libraries under GPL license, but usually javascipt libraries uses MIT license. It's possible to do technically, but it's prohibited by Drupal.org rules: 

https://www.drupal.org/about/licensing

Let's submit FlipDown library on packagist.org:

https://packagist.org/packages/submit

Submit new library on packagist.org

If you submitted library and forgot to add composer.json with "type": "drupal-library", don't worry, just add composer.json file and create new tag for your git repository. This tag will be automatically pushed on packagist.

Here is packagist library page for FlipDown:

https://packagist.org/packages/levmyshkin/flipdown

Packagist library

Be sure that you have drupal-library type on packagist page.

Let's go back to our drupal files and copy ept_text folder, I will name new module ept_countdown:

EPT Countdown

We need to:

- remove configs in /config/install, we will export new configs later

- replace all ept_text mention with ept_countdown

- rename files to have "countdown" instead of "text"

- update texts for module description in ept_countdown.info.yml and README.md files.

I will commit each step separately in git, so you could see updates step by step:

git clone https://git.drupalcode.org/project/ept_countdown.git

Now we have boilerplate for our module and can push changes on Drupal.org.

Create module project on Drupal.org

Let's go on drupal.org site on adding project page:

https://www.drupal.org/node/add

Drupal.org add content

We need to add Module project:

https://www.drupal.org/node/add/project-module

Name: Extra Paragraph Types (EPT): Countdown
Project type: Full project
Short name: ept_countdown
Maintenance status: Actively maintained
Development status: Under active development
Module categories: Content, Content Display
Ecosystem: Extra Paragraph Types (EPT): Core

Create new drupal project

In Description field I usually insert full list of available EPT modules:

# Extra Paragraph Types (EPT): Countdown

Extra Paragraph Types: Countdown module provides ability to add animated countdown
for specific date.

For a full description of the module, visit the
[project page](https://www.drupal.org/project/ept_countdown).

Submit bug reports and feature suggestions, or track changes in the
[issue queue](https://www.drupal.org/project/issues/ept_countdown).


## Table of contents

- Requirements
- Recommended modules
- Installation
- Configuration
- Troubleshooting
- FAQ
- Maintainers


## Requirements

This project requires the following items:

- [EPT Core](https://www.drupal.org/project/ept_core)
- [Paragraphs](https://www.drupal.org/project/paragraphs)

EPT Modules use Media module with Media Image type for background images.
Check Media Image type exists before install EPT Core module.


## Recommended modules

EPT modules provide ability to add different paragraphs in few clicks.
You can install separate paragraph types from this bunch of EPT modules:
- [EPT Accordion / FAQ](https://www.drupal.org/project/ept_accordion)
- [EPT Basic Button](https://www.drupal.org/project/ept_basic_button)
- [EPT Bootstrap Button](https://www.drupal.org/project/ept_bootstrap_button)
- [EPT Call to Action](https://www.drupal.org/project/ept_cta)
- [EPT Carousel](https://www.drupal.org/project/ept_carousel)
- [EPT Countdown](https://www.drupal.org/project/ept_countdown)
- [EPT Counter](https://www.drupal.org/project/ept_counter)
- [EPT Image](https://www.drupal.org/project/ept_image)
- [EPT Image Gallery](https://www.drupal.org/project/ept_image_gallery)
- [EPT Micromodal](https://www.drupal.org/project/ept_micromodal)
- [EPT Quote](https://www.drupal.org/project/ept_quote)
- [EPT Slick Slider](https://www.drupal.org/project/ept_slick_slider)
- [EPT Slideshow](https://www.drupal.org/project/ept_slideshow)
- [EPT Stats](https://www.drupal.org/project/ept_stats)
- [EPT Tabs](https://www.drupal.org/project/ept_tabs)
- [EPT Text](https://www.drupal.org/project/ept_text)
- [EPT Tiles](https://www.drupal.org/project/ept_tiles)
- [EPT Timeline](https://www.drupal.org/project/ept_timeline)
- [EPT Video](https://www.drupal.org/project/ept_video)
- [EPT Video and Image gallery](https://www.drupal.org/project/ept_video_and_image_gallery)
- [EPT Webform](https://www.drupal.org/project/ept_webform)
- [EPT Webform Popup](https://www.drupal.org/project/ept_webform_popup)

More about EPT paragraphs read on EPT Core module page:
[EPT Core](https://www.drupal.org/project/ept_core)

If you are looking for Extra Block Types, You can check out
this bunch of EBT modules:
- [EBT Accordion / FAQ](https://www.drupal.org/project/ebt_accordion)
- [EBT Basic Button](https://www.drupal.org/project/ebt_basic_button)
- [EBT Bootstrap Button](https://www.drupal.org/project/ebt_bootstrap_button)
- [EBT Call to Action](https://www.drupal.org/project/ebt_cta)
- [EBT Carousel](https://www.drupal.org/project/ebt_carousel)
- [EBT Countdown](https://www.drupal.org/project/ebt_countdown)
- [EBT Counter](https://www.drupal.org/project/ebt_counter)
- [EBT Image](https://www.drupal.org/project/ebt_image)
- [EBT Image Gallery](https://www.drupal.org/project/ebt_image_gallery)
- [EBT Micromodal](https://www.drupal.org/project/ebt_micromodal)
- [EBT Quote](https://www.drupal.org/project/ebt_quote)
- [EBT Slick Slider](https://www.drupal.org/project/ebt_slick_slider)
- [EBT Slideshow](https://www.drupal.org/project/ebt_slideshow)
- [EBT Stats](https://www.drupal.org/project/ebt_stats)
- [EBT Tabs](https://www.drupal.org/project/ebt_tabs)
- [EBT Text](https://www.drupal.org/project/ebt_text)
- [EBT Tiles](https://www.drupal.org/project/ebt_tiles)
- [EBT Timeline](https://www.drupal.org/project/ebt_timeline)
- [EBT Video](https://www.drupal.org/project/ebt_video)
- [EBT Video and Image gallery](https://www.drupal.org/project/ebt_video_and_image_gallery)
- [EBT Webform](https://www.drupal.org/project/ebt_webform)
- [EBT Webform Popup](https://www.drupal.org/project/ebt_webform_popup)

More about EBT blocks read on EBT Core module page:
[EBT Core](https://www.drupal.org/project/ebt_core)

If you want to enhance UI for adding Blocks from Layout builder try this module:
[Layout Builder Modal](https://www.drupal.org/project/layout_builder_modal)

If you want to enhance UI for adding Paragraphs try these modules:
[Paragraphs Edit](https://www.drupal.org/project/paragraphs_edit)
[Paragraphs Modal Edit](https://www.drupal.org/project/paragraphs_modal_edit)


## Installation

Install as you would normally install a contributed Drupal module. For further
information, see
[Installing Drupal Modules](https://www.drupal.org/docs/extending-drupal/installing-drupal-modules).


## Configuration


After enable this project will be available a new paragraph type called `EPT Countdown`

When you're creating this new custom paragraph, you can see that there is a new Tab
called `Settings` and there you can configure the Design options inside of
`Settings` tab

You can configure there:
- Margin;
- Border;
- Padding;
- Border Color;
- Border Style;
- Border Radius;
- Background Color;
- Background Image Style;
- Edge to Edge;
- Container Max Width.

EPT Core has configuration form with Primary/Secondary colors and
Mobile/Tablet/Desktop breakpoints in:
Administration » Configuration » Content authoring » Extra Paragraph Types (EPT)
settings

These configs will be applied to other EPT paragraphs by default.

## Troubleshooting

- If you see the error during EPT modules installation: "The EPT Carousel needs
  to be created "Image" media type. (Currently using Media type Image version
  Not created)"
  Then you need to create Image media type:
  Structure » Media types » Add media type

## FAQ

**Q: Can I use only one EPT paragraph type,
for example EPT Slideshow, without other modules?**

**A:** Yes, sure. EPT paragraph types tend to be standalone modules. You can install
only needed paragraph types.


## Maintainers

- Ivan Abramenko - [levmyshkin](https://www.drupal.org/u/levmyshkin)
- Narine Tsaturyan - [Narine_Tsaturyan](https://www.drupal.org/u/narine_tsaturyan)

Now we have Drupal module project page on Drupal.org:
https://www.drupal.org/project/ept_countdown

On Version Control tab you can see instructions how to add remote origin for you local git repository:

https://www.drupal.org/project/ept_countdown/git-instructions

Drupal project version control

After initial commit you should create a new branch to follow major version of other EPT modules, now it's 1.4.x.

Now we can start to add new functionality for our module. The process is similar with custom module development, we will create paragraph type, add fields, include css/js assets.

Start building EPT Countdown functionality

Step 1. Creating EPT Countdown paragraph type. Just install module if you generated it with Drush.

Just install module if you generated it with Drush.

First of all, we need to create a new paragraph type EPT Countdown:

/admin/structure/paragraphs_type/add

Add EPT Countdown paragraph type

It's required that machine name starts from ept_, so usually I name paragraph types starts from EPT, then machine name is automatically being generated in right way. Should paragraph type machine name match to module name? Yes, it's good to have for consistancy and be sure that another EPT module didn't have the same machine name. Paragraph type machine name must start from ept_, becuase it's needed to override templates in modules instead of theme folder, see ept_core_theme_registry_alter() function in ept_core module.

Now we can add EPT settings field, it's needed for all EPT modules. We should add existing field EPT Settings: field_ept_settings:

Add EPT Settings field

 

EPT Settings is common field from EPT Core module, it provides DOM Box, background, spacing and width settings.

As we need countdown to the date, we should have timestamp date field. Let's add it too:

 

I added ept_ for machine-readable name, but it's not required here. It can be field_countdown_date as well. We also have default body and title field for paragraph, so it will be enough for countdown paragraph.

Add Date field

Usually for EPT modules we have horizontal tabs on Edit form:

Horizontal tabs

It's not required, but it's good to split content and settings, because we have a lot of settings for paragraphs. 

Parent field group should be Tabs with settings Direction Horizontal, and Width Breakpoint 120 (or any small number): 

Tabs settings

Now we have a paragraph type, let's enable EPT Countdown module, so templates for paragraph type will be applied:

/admin/modules

Enable EPT Countdown

 

Enable EPT Countdown paragraph for your Content Type with paragraph field. 

Create paragraph EPT Countdown

And what we have on the page:

EPT Countdown

Step 2. Include 3rd party libraries to EPT modules

Now we can include our 3rd party library. We have levmyshkin/flipdown library in composer.json, but we have this new module as custom module, so we need to install this library manually with composer:

composer require levmyshkin/flipdown

New library should be placed in libraries folder automatically:

Install flipdown

Let's add ept_countdown.libraries.yml file and include there flipdown css/js and javascript file ept_flipdown/js/ept_countdown.js where later we will init flipdown plugin:

ept_countdown.libraries.yml

ept_countdown:
  css:
    component:
      /libraries/flipdown/dist/flipdown.min.css: { minified: true }
  js:
    /libraries/flipdown/dist/flipdown.min.js: { minified: true }
    js/ept_countdown.js: {}

For files in /libraries folder we use slash at the beginning, so it's absolute path.

js/ept_countdown.js:

(function ($, Drupal) {

  /**
   * EBT Countdown behavior.
   */
  Drupal.behaviors.eptCountDown = {
    attach: function (context, settings) {

    };
  }

})(jQuery, Drupal);

And we should include new ebt_countdown library in temlates, don't forget what we have two templates:

{{ attach_library('ept_countdown/ept_countdown') }}

Drupal templates

Clear caches and check that your javascript files on the page:

Add javascript file

We will pass date from PHP to javascript in drupalSettings. So we should extend our ebt_countdown.libraries.yml file with dependencies. Also we will use once() function:

  dependencies:
    - core/once
    - core/drupalSettings

Step 3. Include own field widget for EPT Settings, pass variables to javascript

 In EPT modules settings are not being passed to javascript by default. We need to override field widget class EptSettingsDefaultWidget:

ept_countdown/src/Plugin/Field/FieldWidget/EptSettingsCountDownWidget.php:

<?php

namespace Drupal\ept_countdown\Plugin\Field\FieldWidget;

use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\ept_core\Plugin\Field\FieldWidget\EptSettingsDefaultWidget;

/**
 * Plugin implementation of the 'ept_settings_countdown' widget.
 *
 * @FieldWidget(
 *   id = "ept_settings_countdown",
 *   label = @Translation("EPT Countdown settings"),
 *   field_types = {
 *     "ept_settings"
 *   }
 * )
 */
class EptSettingsCountDownWidget extends EptSettingsDefaultWidget {

  /**
   * {@inheritdoc}
   */
  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    $element = parent::formElement($items, $delta, $element, $form, $form_state);

    $element['ept_settings']['pass_options_to_javascript'] = [
      '#type' => 'hidden',
      '#value' => TRUE,
    ];

    $element['ept_settings']['color_theme'] = [
      '#title' => $this->t('Color theme'),
      '#type' => 'radios',
      '#options' => [
        'dark' => $this->t('Dark'),
        'light' => $this->t('Light'),
      ],
      '#default_value' => $items[$delta]->ept_settings['color_theme'] ?? 'dark',
      '#description' => $this->t('Select color theme for countdown'),
      '#weight' => '3',
    ];

    $element['ept_settings']['styles'] = [
      '#title' => $this->t('Styles'),
      '#type' => 'radios',
      '#options' => [
        'default' => $this->t('Default'),
        'new_year' => $this->t('New Year'),
      ],
      '#default_value' => $items[$delta]->ept_settings['styles'] ?? 'default',
      '#description' => $this->t('Select special style for countdown'),
      '#weight' => '4',
    ];

    $element['ept_settings']['heading_days'] = [
      '#title' => $this->t('Heading Days'),
      '#type' => 'textfield',
      '#default_value' => $items[$delta]->ept_settings['heading_days'] ?? $this->t('Days'),
      '#description' => $this->t('Header for Days counter'),
      '#weight' => '5',
    ];

    $element['ept_settings']['heading_hours'] = [
      '#title' => $this->t('Heading Hours'),
      '#type' => 'textfield',
      '#default_value' => $items[$delta]->ept_settings['heading_hours'] ?? $this->t('Hours'),
      '#description' => $this->t('Header for Hours counter'),
      '#weight' => '6',
    ];

    $element['ept_settings']['heading_minutes'] = [
      '#title' => $this->t('Heading Minutes'),
      '#type' => 'textfield',
      '#default_value' => $items[$delta]->ept_settings['heading_minutes'] ?? $this->t('Minutes'),
      '#description' => $this->t('Header for Minutes counter'),
      '#weight' => '7',
    ];

    $element['ept_settings']['heading_seconds'] = [
      '#title' => $this->t('Heading Seconds'),
      '#type' => 'textfield',
      '#default_value' => $items[$delta]->ept_settings['heading_seconds'] ?? $this->t('Seconds'),
      '#description' => $this->t('Header for Seconds counter'),
      '#weight' => '8',
    ];

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
    foreach ($values as &$value) {
      $value += ['ept_settings' => []];
    }
    return $values;
  }

}

Now we can select this field widget for EPT Settings field:

/admin/structure/paragraphs_type/ept_countdown/form-display

EPT Settings

Let's resave our paragraph and check drupalSettings javascript variable. Now all options from EPT Settings field will be passed in javascript:

Drupal EPT

 

At the end of paragraph-id-* we have Paragraph ID, so we have unique keys for all paragraphs.

FlipDown plugin has option for light/dark theme, so let's pass this setting field in our field widget EptSettingsCountDownWidget:

    $element['ept_settings']['color_theme'] = [
      '#title' => $this->t('Color theme'),
      '#type' => 'radios',
      '#options' => [
        'dark' => $this->t('Dark'),
        'light' => $this->t('Light'),
      ],
      '#default_value' => $items[$delta]->ept_settings['color_theme'] ?? 'dark',
      '#description' => $this->t('Select color theme for countdown'),
      '#weight' => '3',
    ];

EPT Countdown settings

Then we can get theme color value in javascript:

Dark theme

Step 4. Initialize FlipDown plugin for EPT Countdown paragraph

We passed variables from settings to javascript, but we need to pass also Date value from content to javascript. We will create empty div with attribute data-date="", where we will place date and time from Date field. We will use paragraph_id to define unique ID for paragraph:

paragraph--ept-countdown--default.html.twig:

    <div
      class="ept-countdown-date ept-countdown-inline-block flipdown"
      id="paragraph-id-{{ paragraph.id() }}"
      data-date="{{ content.field_ept_countdown_date[0]['#attributes']['datetime']|date('U') }}">
    </div>

EPT HTML

 

If you are not sure about where field data is, you can install Twig Debugger module and print content.field_ept_countdown_date on the page {{ dump(content.field_ept_countdown_date) }}:

https://www.drupal.org/project/twig_debugger

And we used date('U') twig filter to transform date to timestamp.

Now we can include custom javascript and initialize FlipDown.

/ept_countdown/js/ept_countdown.js:

(function ($, Drupal) {

  /**
   * EPT Countdown behavior.
   */
  Drupal.behaviors.eptCountDown = {
    attach: function (context, settings) {
      var countdowns = once('ept-countdown-paragraph', '.ept-countdown-date', context);
      countdowns.forEach(function(countdown) {
        var eptOptions = drupalSettings['eptCountdown'][countdown.getAttribute('id')];
        var countdownTimestamp = parseInt(countdown.getAttribute('data-date'));
        var countdownId = countdown.getAttribute('id');

        new FlipDown(countdownTimestamp, countdownId, {
          theme: eptOptions['options']['color_theme'],
        }).start();
      });
    }
  }

})(jQuery, Drupal);

Don't forget to clear caches to see updates. After that FlipDown should be working on the page:

FlipDown

Step 5. Styling new EPT Countdown paragraph. Gulp.js file is included for drush generated EPT module.

As you see even default FlipDown styles are not working great. There are two lines of numbers even on desktop. But we can easily fix it with custom styles. You can copy gulpfile.js and package.json files for compiling scss to css from EPT Counter or EPT Core Kickstarter module:

https://www.drupal.org/project/ept_counter

gulpfile.js:

// --------------------------------------------------
// Load Plugins
// --------------------------------------------------

var gulp = require('gulp'),
    sass = require('gulp-dart-scss'),
    postcss = require("gulp-postcss"),
    autoprefixer = require("autoprefixer"),
    cssnano = require("cssnano"),
    notify = require('gulp-notify'),
    sassUnicode = require('gulp-sass-unicode');


var config = {
    // main scss files that import partials
    scssSrc: 'scss/*.scss',
    // all scss files in the scss directory
    allScss: 'scss/**/*.scss',
    // the destination directory for our css
    cssDest: 'css/',
    // all js files the js directory
    allJs: 'assets/js/**/*.js',
    // all img files
    allImgs: 'assets/img/**/*'
};


// Define tasks after requiring dependencies
function style() {

  return gulp.src(config.allScss)
    .pipe(sass())
    .pipe(sassUnicode())
    .pipe(postcss([autoprefixer()]))
    .pipe(gulp.dest(config.cssDest));

  gulp.task('sass:watch', function () {
    gulp.watch('./scss/**/*.scss', ['sass']);
  });
}

// Expose the task by exporting it
// This allows you to run it from the commandline using
// $ gulp style
exports.style = style;

function watch(){
    // gulp.watch takes in the location of the files to watch for changes
    // and the name of the function we want to run on change
    gulp.watch('scss/**/*.scss', style)
}

// Don't forget to expose the task!
exports.watch = watch

package.json:

{
  "name": "ept_styles",
  "version": "1.0.0",
  "description": "Run npm install and then gulp watch",
  "main": "gulpfile.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "autoprefixer": "^10.2.5",
    "cssnano": "^5.0.2",
    "gulp": "^4.0.2",
    "gulp-dart-scss": "^1.1.0",
    "gulp-notify": "^4.0.0",
    "gulp-postcss": "^9.0.0",
    "gulp-sass-unicode": "^1.0.5",
    "gulp-sourcemaps": "^3.0.0"
  },
  "dependencies": {
    "cucumber": "*",
    "postcss": "^8.2.13"
  }
}

package-lock.json file will be generated after you run:

npm install

And you can start gulp task with:

gulp watch

Now we will add scss files:

/ept_countdown/scss/flipdown.scss

.flipdown {
  width: 580px;
}

So flipdown.css file will be automatically generated from flipdown.scss. And we can include .css file in .libraries.yml:

ept_countdown:
  css:
    component:
      /libraries/flipdown/dist/flipdown.min.css: { minified: true }
      css/flipdown.css: { }

Let's clear cache and see results:

EBT countdown

Now it looks better!

Can we use plain css without compiling from scss?

Yes, we can, but writing scss is more convient for the most developers.

Step 6. Extend settings form with FlipDown plugin options

FlipDown plugin has few options to change display:

https://github.com/PButcher/flipdown

  • theme
  • headings

We already created new field widget for EPT Settings EptSettingsCountDownWidget, now we will extend this form with new fields:

/ept_countdown/src/Plugin/Field/FieldWidget/EptSettingsCountDownWidget.php:

public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    $element = parent::formElement($items, $delta, $element, $form, $form_state);

    $element['ept_settings']['pass_options_to_javascript'] = [
      '#type' => 'hidden',
      '#value' => TRUE,
    ];

    $element['ept_settings']['color_theme'] = [
      '#title' => $this->t('Color theme'),
      '#type' => 'radios',
      '#options' => [
        'dark' => $this->t('Dark'),
        'light' => $this->t('Light'),
      ],
      '#default_value' => $items[$delta]->ept_settings['color_theme'] ?? 'dark',
      '#description' => $this->t('Select color theme for countdown'),
      '#weight' => '3',
    ];

    $element['ept_settings']['styles'] = [
      '#title' => $this->t('Styles'),
      '#type' => 'radios',
      '#options' => [
        'default' => $this->t('Default'),
        'new_year' => $this->t('New Year'),
      ],
      '#default_value' => $items[$delta]->ept_settings['styles'] ?? 'default',
      '#description' => $this->t('Select special style for countdown'),
      '#weight' => '4',
    ];

    $element['ept_settings']['heading_days'] = [
      '#title' => $this->t('Heading Days'),
      '#type' => 'textfield',
      '#default_value' => $items[$delta]->ept_settings['heading_days'] ?? $this->t('Days'),
      '#description' => $this->t('Header for Days counter'),
      '#weight' => '5',
    ];

    $element['ept_settings']['heading_hours'] = [
      '#title' => $this->t('Heading Hours'),
      '#type' => 'textfield',
      '#default_value' => $items[$delta]->ept_settings['heading_hours'] ?? $this->t('Hours'),
      '#description' => $this->t('Header for Hours counter'),
      '#weight' => '6',
    ];

    $element['ept_settings']['heading_minutes'] = [
      '#title' => $this->t('Heading Minutes'),
      '#type' => 'textfield',
      '#default_value' => $items[$delta]->ept_settings['heading_minutes'] ?? $this->t('Minutes'),
      '#description' => $this->t('Header for Minutes counter'),
      '#weight' => '7',
    ];

    $element['ept_settings']['heading_seconds'] = [
      '#title' => $this->t('Heading Seconds'),
      '#type' => 'textfield',
      '#default_value' => $items[$delta]->ept_settings['heading_seconds'] ?? $this->t('Seconds'),
      '#description' => $this->t('Header for Seconds counter'),
      '#weight' => '8',
    ];

    return $element;
  }

Update settings form

So we can use Headings for translations:

Add headings

We have all EPT settings values in javascript, so I just added ID in template for easier getting right unique key.

.setAttribute('id', 'paragraph-countdown-id-' ~ paragraph.id())

Options keys have pattern the same with IDs:

/ept_core/ept_core.module:

  $build['#attached']['drupalSettings'][$bundle]['paragraph-id-' . $paragraph_id] = $paragraph_options;

So we could use options from javascript drupalSettings:

EPT options

/ept_countdown/js/ept_countdown.js:

(function ($, Drupal) {

  /**
   * EPT Countdown behavior.
   */
  Drupal.behaviors.eptCountDown = {
    attach: function (context, settings) {
      var countdowns = once('ept-countdown-paragraph', '.ept-countdown-date', context);
      countdowns.forEach(function(countdown) {
        var eptOptions = drupalSettings['eptCountdown'][countdown.getAttribute('id')];
        var countdownTimestamp = parseInt(countdown.getAttribute('data-date'));
        var countdownId = countdown.getAttribute('id');

        new FlipDown(countdownTimestamp, countdownId, {
          theme: eptOptions['options']['color_theme'],
          headings: [
            eptOptions['options']['heading_days'],
            eptOptions['options']['heading_hours'],
            eptOptions['options']['heading_minutes'],
            eptOptions['options']['heading_seconds'],
          ],
        }).start();
      });
    }
  }

})(jQuery, Drupal);

I also added $element['ept_settings']['styles'] in field widget, so we will use it in template for adding new class for entire paragraph.

{%
  set classes = [
  'paragraph',
  'ept-paragraph',
  'ept-paragraph-countdown',
  'paragraph--type--' ~ paragraph.bundle|clean_class,
  'ept-paragraph--type--' ~ paragraph.bundle|clean_class,
  view_mode ? 'paragraph--view-mode--' ~ view_mode|clean_class,
  not paragraph.isPublished() ? 'paragraph--unpublished',
  'paragraph-id-' ~ paragraph.id(),
  content.field_ept_settings['#object'].field_ept_settings.ept_settings.styles,
  content.field_ept_settings['#object'].field_ept_settings.ept_settings.color_theme,
]
%}
{% if content.field_ept_settings['#object'].field_ept_settings.ept_settings.styles == 'new_year' %}
  {{ attach_library('ept_countdown/new_year') }}
{% endif %}

 It will include new_year library for New Year style, but now we need to create this library.

/ept_countdown/ept_countdown.libraries.yml:

new_year:
  css:
    component:
      css/new-year.css: { }

And here is styles for new-year paragraph:

/ept_countdown/scss/new-year.scss
/ept_countdown/css/new-year.css

.ept-paragraph-countdown.new_year {
  background: url(../img/snowflakes.webp) center center repeat;
}

Here is result of adding new style:

New Year EBT block

You can add any number of styles for new or existing EPT modules. You can also suggest own styles for any EPT module, just create issue on drupal.org for it:

https://www.drupal.org/project/issues/ept_core

Step 7. Export configs for EPT paragraph types and fields

  

I think we finished with adding features to EPT Countdown, it's time to export configs and deploy changes on Drupal.org. We should copy all configs related to EPT Countdown to /ept_countdown/config/install folder.

If you generated EPT module with Drush, you need to update configs for new fields and your EPT paragraph type.

After that enable EPT module on Extend page /admin/modules. New EPT paragraph type and other settings will be installed from /config/install folder from config files:

 

EBT module configs

We don't need to add language.* configs, because some drupal sites have only one language and Language module can be disabled.

Usually I copy all files and checking that I have copy in config/install folder.

Copy of configs

Now we need to remove uuid and hashes from configs in config/install folder

Remove uuid

As we used another Drupal modules we should include them in .info file as dependencies.

Drupal dependencies

/ept_countdown/ept_countdown.info:

dependencies:
  - drupal:datetime

Step 8. Deploy on Drupal.org and testing

We created new project on Drupal.org before:

https://www.drupal.org/project/ept_countdown

I will use 1.4.x branch as main branch, for consistancy with other EPT modules:

Drupal EBT module

So all releases will be started from 1.4.0 versions:

git tag 1.4.0
git push origin 1.4.0

You can also create -alpha, -beta version before creating stable 1.4.0 version.

We need to wait 10 days before module can be opted into security advisory coverage.

EBT Countdown

So we can test our new module and fix bugs.

Step 9. Add README.md file

If you generated EPT module with Drush than Readme file is already there.

Don't forget to add README.md file, you can see example in other EPT modules:

https://www.drupal.org/project/ept_slideshow

Thank you for using EPT modules! Feel free to ask or suggest any ideas:

Create an issue on Drupal.org

Contact EPT modules developer

Or message me on LinkedIn