As mentioned in part 1, we had trouble getting JavaScript to play nicely with both Drupal and Storybook. At the beginning of our process, we testing the connection with console.log()
statements, but when it came to actually adding interaction, using JS in Storybook was a no-go.
Skibinski suggests a method using a Babel plugin to wrap existing JavaScript. I had mixed results testing with this and ultimately didn’t like that I would have a separate directory for Drupal to reference. Ideally if Drupal is referencing a Twig file from one directory, I’d like it to be able to reference the JS file in that same directory. Plus, I’d like to be saved the trouble of having to run a separate command to re-process the JS files specifically for Drupal.
Ultimately I came up with a solution that allows developers to write ES6 classes that both Storybook and Drupal can utilize. Rikki Bochow shared a guide on removing jQuery from Drupal themes that I referenced while working on this particular setup. Using jQuery vs. ES6 is a different conversation, but I did use ES6 here.
First things first, let’s write some JavaScript.
Below is a very basic constructor that will change the style of a block when based on mouseover/mouseout and will log this
(the block element we are affecting) to the console.
export class BlockScript {
// The block parameter is passed by Drupal in below code.
// It's passed by Storybook within the useEffect block.
constructor(block) {
// We need to add the block parameter being passed onto "this" within our class.
Object.defineProperty(this, 'block', { value: block || null });
this.bindEvents();
}
bindEvents() {
this.block.addEventListener('mouseover', this.onMouseover);
this.block.addEventListener('mouseout', this.onMouseout);
}
onMouseover() {
console.log(this);
this.setAttribute('style', 'color:red; border: 1px solid blue;');
}
onMouseout() {
console.log(this);
this.setAttribute('style', 'color:black; border: none;');
}
}
To wrap this class function in Drupal behaviors, I could simply add the behaviors attach a function to the bottom of the file, passing the block as needed and call the constructor function from my block.stories.js
file.
(({ behaviors }, { theme_name }) => {
behaviors.button = {
attach(context) {
context.querySelectorAll('.block').forEach(block => {
new BlockScript(block);
});
},
};
})(Drupal, drupalSettings);
However, if I try to preview that same code in Storybook, I get: Drupal is not defined
.
There is the option of the aforementioned Babel CLI method, or I could explore passing Drupal
in some way to Storybook. As developers I sometimes feel we skip to the most complicated solutions and overlook simple solutions:
I can just tell the code to stop executing before it hits the Drupal behaviors block.
// Only run Drupal code if Drupal is defined.
if (typeof Drupal !== 'undefined') {
(({ behaviors }, { theme_name }) => {
behaviors.button = {
attach(context) {
context.querySelectorAll('.block').forEach(block => {
new BlockScript(block);
});
},
};
})(Drupal, drupalSettings);
}
On the Storybook side, I’ll make use of useEffect()
to bring in my class function from the js
file.
storiesOf('Organisms/Blocks', module).add('Block', () => {
useEffect(() => {
document.querySelectorAll('.block').forEach(block => {
// eslint-disable-next-line
new BlockScript(block);
});
}, []);
return Block(blockData);
});
In short, Drupal is using behaviors to attach the function, and Storybook is using useEffect()
`. In the end, I get the same JavaScript result in Storybook:
And in Drupal: