marzeelabs / besugo

Boilerplate for MZ version of hugo + netlifyCMS
https://besugo.marzeelabs.org
10 stars 0 forks source link

Feature/#27 jsx layouts #29

Closed Quicksaver closed 6 years ago

Quicksaver commented 6 years ago

Closes #27: resolves existing layout redundancy.

NOTE: this was branched out of #26, as of its creation still not merged, because it requires the webpack changes from there.

Overview

Any piece of layout code that can potentially be shared between pages, or even repeated inside the same page, should go in its own BesugoComponent in a .jsx file. This way, we won't have to copy-paste repeating layouts in every file, or even rewrite it for CMS previews.

BesugoComponent

A Class extension of React.Component, what matters most to us is its ability to parse JSX from any data supplied to it, and that it can reuse other components when rendering itself.

All such components should have the following basic structure to be built:

import React from 'react';
import BesugoComponent from 'Besugo';

class SomeThing extends BesugoComponent {
  // ...
}

SomeThing.initialize();
export default SomeThing;

Below is a structural overview of a BesugoComponent extended class.

constructor

constructor(props) {
  super(props);
}

Only mandatory if the component expects to be passed any props (i.e. data from attributes in html). Should always take the form shown above.

config

A static object that defines this component within our website.

Example:

static get config() {
  return {
    tag: "Person",
    categories: [ "people", "people-pt" ]
  };
}

getData()

An optional method to fetch and return whatever data is needed to build the component. If in a CMS preview page, it should fetch the data from the methods supplied by the CMS global object; otherwise it would fetch from its props object.

Use this.isPreview() to check whether we're in a CMS preview content.

Example:

getData() {
  if(this.isPreview()) {
    const entry = this.props.entry;
    return {
      Title: entry.getIn(['data', 'title']),
      Content: this.props.widgetFor('body')
    };
  }
  return this.props;
}

In most cases, you would pass any necessary data as attributes of that placeholder, and they will be direct properties of the this.props object:

<Person
  firstname="Foo"
  lastname="Bar"
></Person>

resolves automatically to

this.props = {
  firstname: "Foo",
  lastname: "Bar"
}

If there's a need, you can pass more complicated data from hugo templates as children of the placeholder element, or as a JSON text inside it. The placeholder DOM object is given to the component as this.props.xplaceholder, so you can pass the data however you want as long as you fetch it appropriately.

Example hugo template:

<BlogPost
  title="{{ .Title }}"
  content="{{ htmlEscape .Content }}"
>
{{ range .Params.people }}
  {{ range where (where $.Site.RegularPages "Section" "people") ".Params.title" .person }}
    <BlogPostAuthor
      link="{{ .URL }}"
      title="{{ .Title }}"
    ></BlogPostAuthor>
  {{ end }}
{{ end }}
</BlogPost>

fetch in the component as:

getData() {
  const data = Object.assign({
    people: []
  }, this.props);

  if(this.props.xplaceholder) {
    const authors = this.props.xplaceholder.querySelectorAll('BlogPostAuthor');
    for(let i = 0; i < authors.length; i++) {
      let author = authors[i];
      let person = {
        link: author.getAttribute('link'),
        Title: author.getAttribute('title')
      };
      data.people.push(person);
    }
  }

  // "Content" comes pre-built with HTML markup already. We need to parse it so that it doesn't show up as simple text
  const parsed = new DOMParser().parseFromString(data.content, "text/html");
  data.content = ReactHtmlParser(parsed.documentElement.textContent);

  return data;
}

renderBlock()

Where the actual layout for this component goes; must return a single JSX object, so if necessary you must encapsulate the whole output in an outer div container or equivalent.

React elements have some syntax differences, the most important being:

Example:

renderBlock() {
  const data = this.getData();
  return (
    <div>
      <div className="blog-post__header" style={{ backgroundImage: `url(${data.image})` }}>
        <div className="blog-post__header-title__wrapper">
          <h1 className="blog-post__header-title">{ data.title }</h1>
        </div>
      </div>
      <section className="layout-container--inner">
        { data.content }
      </section>
    </div>
  );
}

Although it's not necessary, when the component will only be used for direct output of a block and not for any content preview, you can define the layout in the render() method instead as you would in a typical React component.

You can also include other components within another component's layouts; don't forget to pass any data necessary to build it:

import React from 'react';
import BesugoComponent from 'Besugo';
import SocialIcons from 'partials/SocialIcons';

// ...

renderBlock() {
  const data = this.getData();

  return (
    <div className="profile__header-info">
      <h1 className="profile__header-info__title">{ data.Title }</h1>
      <SocialIcons section="profile" { ...data } />
    </div>
  );
}

renderPreview()

This is the layout for the preview area of this component. Typically, you will encapsulate the above renderBlock() within another JSX layout:

import React from 'react';
import BesugoComponent from 'Besugo';
import SVGElements from 'partials/SVGElements';
import TopHeader from 'partials/TopHeader';
import EndFooter from 'partials/EndFooter';

// ...

renderPreview() {
  return (
    <div id="cmsPreview">
      <SVGElements/>
      <TopHeader/>
      { this.renderBlock() }
      <EndFooter/>
    </div>
  );
}