Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Dom Helpers for more concise block code #210

Closed
wants to merge 1 commit into from

Conversation

andreituicu
Copy link

@andreituicu andreituicu commented Apr 24, 2023

Context

This PR adds a small JS file to help with DOM elements generation.
We've been using this in our project and it speeds up a lot the development, code reviews and testing, so I thought it might be useful for new projects as well.

hlxsites/moleculardevices#161

Why?

The following Dom Helper functions allow for:

  • a more concise code
  • composable structure
  • dom like syntax structure to easily visualise the resulting HTML when looking at the code
  • faster development and code review of block code
  • while maintaining 100 LHS for performance
  • 100% vanilla JS / no compilation required
  • with minimal overhead - a few ifs and function calls

Main Usecases

  • DOM rendering of dynamic data from spreadsheets and indexes
  • Adding new DOM elements during block decoration to what Franklin provides, based on the word document (e.g. adding buttons for a carousel, where the elements are coming from the word document)

Examples

It allows rewrite from:

function renderItem(item) {
  const newsItem = document.createElement('div');
  newsItem.classList.add('blog-carousel-item');
  const newsItemLink = document.createElement('a');
  newsItemLink.href = item.path;
  newsItem.append(newsItemLink);
  const newsThumb = document.createElement('div');
  newsThumb.classList.add('blog-carousel-thumb');
  newsThumb.append(createOptimizedPicture(item.image, item.title, 'lazy', [{ width: '800' }]));
  newsItemLink.appendChild(newsThumb);
  const newsCaption = document.createElement('div');
  newsCaption.classList.add('blog-carousel-caption');
  newsCaption.innerHTML = `
    <h3>${item.title}</h3>
    <p class="blog-description">${item.description}</p>
    <p class="button-container">
      <a href=${item.path} aria-label="Read More" class="button primary">Read More</a>
    </p>
  `;
  newsItemLink.appendChild(newsCaption);
  return newsItem;
}

To:

function renderItem(item) {
  return (
    div({ class: 'blog-carousel-item' },
      a({ href: item.path },
        div({ class: 'blog-carousel-thumb' },
          createOptimizedPicture(item.image, item.title, 'lazy', [{ width: '800' }]),
        ),
        div({ class: 'blog-carousel-caption' },
          h3(item.title),
          p({ class: 'blog-description' }, item.description),
          p({ class: 'button-container' },
            a({ href: item.path, 'aria-label': 'Read More', class: 'button primary' }, 'Read More'),
          ),
        ),
      ),
    )
  );
}

More examples from our project code:

Other projects

The reason why I thought it would be a good addition to the boiler plate is that I found similar/partial implementations, some repeating between projects, some independently written.

  1. Adobe.com: https://blog.adobe.com/blocks/block-helpers.js
  2. Volvo Trucks: https://github.com/hlxsites/vg-volvotrucks-us/blob/main/scripts/scripts.js#L40-L53
  3. Caesars: https://github.com/hlxsites/caesars/blob/main/caesars-palace/scripts/scripts.js#L244-L263
  4. Airlessco: https://github.com/hlxsites/airlessco/blob/main/scripts/scripts.js#L119-L134
  5. WKND: https://github.com/hlxsites/wknd/blob/main/scripts/scripts.js#L50-L69
  6. Mammotome: https://github.com/hlxsites/mammotome/pull/67/files#diff-a5ee87ff9063f921ba01933977e6164e5729551986030f0ed7eb747bcffb740fR228 (Thank you, @karlpauls!)

Credits

This is of course inspired by the JSX (React) syntax, articles and code pieces explaining its internals.

@aem-code-sync
Copy link

aem-code-sync bot commented Apr 24, 2023

Hello, I'm Franklin Bot and I will run some test suites that validate the page speed.
In case there are problems, just click the checkbox below to rerun the respective action.

  • Re-run PSI Checks

@@ -17,5 +17,6 @@ module.exports = {
'import/extensions': ['error', {
js: 'always',
}],
'function-paren-newline': 'off',
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is so you can keep the attribute object on the same line as the function call, to make visually similar with the dom.

p({ class: 'button-container' },
  'Paragraph Text'
);

@jose-correia
Copy link

jose-correia commented Apr 24, 2023

Adding a 👍 on this.

Using these DOM helpers highly increased the readability and development speed of our project. It reduces the complexity of the blocks and improves our capacity to understand the final structure just by looking at the JS.

@karlpauls
Copy link
Contributor

👍 - @solaris007 and I are working on something similar (arguably not as sophisticated as this proposal) in [0]. I think it would make some sense to have a standard way to create nested Dom structures in a readable way.

[0] https://github.com/hlxsites/mammotome/pull/67/files#diff-a5ee87ff9063f921ba01933977e6164e5729551986030f0ed7eb747bcffb740fR228

@dylandepass
Copy link
Member

@andreituicu I'm curious what limitations in template literals made you come up with this approach?

One benefit I can see is it's easier to attach event listeners and maybe less need for innerHTML, but besides that I'm not sure I see what else you get.

I would 100% agree that it's much easier to read and maintain than imperative DOM code. But on the flip side I think the same can be done with string template literals (and still have it be concise. composable and easy to visualize).

function renderItem(item) {
  return /* html */`
    <div class='blog-carousel-item'>
      <a href=${item.path}>
        <div class='blog-carousel-thumb'>
          ${createOptimizedPicture(item.image, item.title, 'lazy', [{ width: '800' }])}
        </div>
        <div class='blog-carousel-caption'>
          <h3>${item.title}</h3>
          <p class='blog-description'>${item.description}</p>
          <p class='button-container'>
            <a href=${item.path} aria-label='Read More' class='button primary'>Read More</a>
          </p>
        </div>
      </a>
    </div>
  `;
}

@karlpauls
Copy link
Contributor

@dylandepass, one problem we had is that as soon as you have user (i.e., author) input to put into the template, it requires escaping (to prevent mistakes and XSS etc.). At that point, the template for innerHtml doesn't work anymore.

@andreituicu
Copy link
Author

andreituicu commented Apr 24, 2023

@dylandepass besides the point that @karlpauls makes about XSS, the snippet you shared will unfortunately not generate the expected DOM. The resulting string will be:

<div class='blog-carousel-item'>
      <a href=/path/to/resource>
        <div class='blog-carousel-thumb'>
          [object HTMLPictureElement]
        </div>
        <div class='blog-carousel-caption'>
          <h3>Title</h3>
          <p class='blog-description'>Description ...</p>
          <p class='button-container'>
            <a href=/path/to/resource aria-label='Read More' class='button primary'>Read More</a>
          </p>
        </div>
      </a>
    </div>

which when you try to set it as the innerHTML of a dom element will give an error like: Cannot create property 'innerHTML' on string . As you can see the result of createOptimizedPicture is converted into a string like [object HTMLPictureElement]. Of course you could use outerHTML like https://stackoverflow.com/questions/54768158/how-to-embed-dom-element-inside-template-literals suggests and even then you might loose event listeners (didn't test, just trusted the source from stackoverflow on this 🙂).

There are other minor things that I like about the approach from this PR, which could be argued are subjective/a matter of taste:

  • avoids the double conversion: DOM -> string -> DOM
  • better syntax highlighting (IDE and Github will flatten a string)
  • IDE and linter will let you know if you didn't close brackets or parentheses (it will not check the content of your string for forgotten unclosed elements)

And just say, I'm not trying to suggest that people should not use string literals. I think each approach has it's place in the code and one does not exclude the other. Personally, I liked this one better and the fact that there were other projects who took the time to create similar implementations suggested that there might be a pattern.

@dylandepass
Copy link
Member

dylandepass commented Apr 24, 2023

@karlpauls Understood, and that makes sense. For the eecol project we ran our template strings through DOMPurify where necessary. You'll take a bit of a hit but should still be possible to get to 100 on PSI.

@andreituicu Correct, my bad, I through the example together quickly without thinking too much about it. I solved this in the past by using outerHTML as you mentioned.

better syntax highlighting (IDE and Github will flatten a string)

One note, you can fix the syntax highlighting and code completion issue in vscode with this extension.

suggested that there might be a pattern

For sure, thanks for clarifying the gaps. I just wanted to better understand the pain points here and see if there was something else I was missing.

@trieloff
Copy link
Contributor

To me, this looks like a templating language and templating languages should be a user choice, not a framework default.

The most vanilla thing that I could imagine is based on the HTML template element https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template – but it's not very widely adopted yet.

@andreituicu
Copy link
Author

andreituicu commented Apr 24, 2023

@trieloff I agree that templating languages should be a user choice. I don't necessarily believe that having something very minimal default in the boilerplate is incompatible with that. My perspective is that it would maybe help in that sense with the pitfall of jumping to a heavy framework for the basic/common usecases and then maybe struggling to obtain a good LHS. Still users which have a different view of what they want to use, can do it without any barriers. Being in a separate file it won't even be loaded by the browser unless used and can be easily and cleanly discarded if not needed.

The way I personally look at this code, is a helper function like createOptimizedPicture or decorateIcons. Users can make use of them, or write their own/bring their own. We don't force them, we simply provide a starting point. So in that sense, although I agree that it can be seen as a templating engine, I wouldn't go as far as calling it as such, because of how simple and lightweight the code is, I look at it as just a helper.

You guys (the core team) have the best overview of all projects and usages, I just thought it might be helpful for others given the feedback from our project and the what I've found in others.

@andreituicu
Copy link
Author

andreituicu commented Apr 24, 2023

@dylandepass

Correct, my bad, I through the example together quickly without thinking too much about it. I solved this in the past by using outerHTML as you mentioned.

Sure, I should've maybe been explicit: I only pointed it to show that, although possible with string literals, the developer experience when writing it could be a little bit more seamless from my point of view, when it comes to the composability aspect.

One note, you can fix the syntax highlighting and code completion issue in vscode with this extension.

Noted! Was not aware! Thank you!

I just wanted to better understand the pain points here and see if there was something else I was missing.

Happy to discuss and thank you very much for the feedback!

@mhaack
Copy link

mhaack commented Apr 25, 2023

While I see the point on not putting this into boilerplate, as @trieloff pointed out it should be a user's choice.

There seems to be a need for this. I'm aware two other projects built helpers similar like this one. We might add this to the block party instead of boilerplate.

@andreituicu
Copy link
Author

@mhaack Thank you for the feedback!

We might add this to the block party instead of boilerplate.

you are referring to https://github.com/adobe/helix-block-collection , correct?
I can open a PR there if that's a more appropriate place for this and contribute a doc entry to https://www.hlx.live/developer/block-collection#block-collection to make it easier discoverable.

@trieloff does that sounds like a good approach to you?

I'm aware two other projects built helpers similar like this one.

just checking: are they captured in the Other Projects section in this PR? if not could you please share them so I can add those as well?

@davidnuescheler
Copy link
Contributor

i agree that the best place would be "block party" mentioned by @mhaack to get started.

the "block party" is a new concept we came up with for community contributed blocks and snippets. i don't think the code should be in any particular repos but it lives where it is... i don't know the latest on process for including things in the block party. maybe it makes sense to track those as issues in https://github.com/adobe/helix-website.
(cc: @amol-anand)

@trieloff
Copy link
Contributor

trieloff commented May 3, 2023

https://github.com/hlxsites/block-party seems like the best place for this.

@davidnuescheler
Copy link
Contributor

should we move this issue to block-party and close this PR?

@andreituicu
Copy link
Author

@davidnuescheler @trieloff Thank you! Created hlxsites/block-party#1 .

Do you mind if I also keep this PR open as well for discoverability reasons? At the moment, not that many people know about the Block Party repository, while I think many check the boilerplate pull requests for possible improvemennts.

@trieloff
Copy link
Contributor

To solve the discoverability issue, you could submit a PR that updates the README with links to block collection and block party.

@andreituicu
Copy link
Author

@trieloff just to clarify I understood correctly: You mean a PR to the README of this repository (the boilerplate)? Sure, will do!

@davidnuescheler
Copy link
Contributor

i am closing this PR, this will still keep it on record of course. keeping it open against this repo is misleading.
the requirement of being included into boilerplate is showing "use" of the DOM utilities proposed here in the vast majority of all projects as stated here: https://www.hlx.live/developer/block-collection#boilerplate

we originally had DOM manipulation utilities in boilerplate and removed them as the produce an additional onboarding hurdle for new devs and usually devs don't agree what they want their DOM manipulation libraries to look like (which is why there are so many :) )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants