mjmlio / mjml

MJML: the only framework that makes responsive-email easy
https://mjml.io
MIT License
17.08k stars 960 forks source link

How to make components that use other mjml components properly? #781

Closed ogonkov closed 6 years ago

ogonkov commented 7 years ago

I'm trying to build complex component, but it seems very hard to manage props across multiple sections and columns.

It's a "raw" component that i want to code

<mj-wrapper
          background-color="#f0f0f0"
          background-url="images/bg.png"
          background-repeat="no-repeat"
          background-size="cover"
          padding="0 28px 84px">
  <mj-section padding="0">
    <mj-group width="680px">
      <mj-column width="50%" vertical-align="middle">
        <mj-text
            padding="23px 0 0"
            font-size="20px">
          {{ username }}
        </mj-text>
      </mj-column>

      <mj-column width="50%">
        <mj-image
            align="right"
            padding="15px 0 0"
            src="images/logo.png"
            width="144px"
            height="36px" />
      </mj-column>
    </mj-group>
  </mj-section>

  <mj-section>
    <mj-group width="680px">
      <mj-column width="100%">
        <mj-text
            line-height="36px"
            align="center"
            padding="28px 66px 0"
            font-size="28px">
          // Some variable text
        </mj-text>
      </mj-column>
    </mj-group>
  </mj-section>
</mj-wrapper>

That is my component for it

import {MJMLElement, helpers} from 'mjml-core';
import React, {Component} from 'react';
import Column from 'mjml-column';
import Group from 'mjml-group';
import Section from 'mjml-section';
import Text from 'mjml-text';
import Wrapper from 'mjml-wrapper';
import Image from 'mjml-image';

const tagName = 'acme-header';
const parentTag = ['mj-container'];
const endingTag = true;
const defaultMJMLDefinition = {
    content: '',
    attributes: {
        name: null,
        'logo-width': null,
        'logo-height': null,
        'logo-alt': null,
        'background-color': '#f0f0f0',
        'total-width': '680px',
        'padding': '0 28px 84px'
    }
};

@MJMLElement
class AcmeHeader extends Component {
    renderGreetingAndLogo() {
        const {mjAttribute} = this.props;
        const {color} = baseText;

        return (
            <Section
                key="header-greeting-and-logo"
                padding="0">
                <Group width="680px">
                    <Column
                        width="50%"
                        vertical-align="middle">
                        <Text
                            css-class="header__greeting"
                            padding="23px 0 0"
                            font-size="20px"
                            font-family={mjAttribute('font-family')}
                            color={color}>
                            {`Hello, ${mjAttribute('name')}`}
                        </Text>
                    </Column>

                    <Column width="50%">
                        <Image
                            align="right"
                            padding="15px 0 0"
                            src={mjAttribute('logo-src')}
                            alt={mjAttribute('logo-alt')}
                            width={mjAttribute('logo-width')}
                            height={mjAttribute('logo-height')}
                        />
                    </Column>
                </Group>
            </Section>
        );
    }

    renderText() {
        const {mjAttribute, mjContent} = this.props;
        const {color} = baseText;

        return (
            <Section key="header-text" padding="0">
                <Group width="680px">
                    <Column width="100%">
                        <Text
                            line-height="36px"
                            align="center"
                            padding="28px 66px 0"
                            font-family={mjAttribute('font-family')}
                            color={color}
                            font-size="28px">
                            {mjContent()}
                        </Text>
                    </Column>
                </Group>
            </Section>
        );
    }

    render() {
        const sections = [
            this.renderGreetingAndLogo(),
            this.renderText()
        ];

        return (
            <Wrapper {...this.props}>
                {sections}
            </Wrapper>
        );
    }
}

AcmeHeader.tagName = tagName;
AcmeHeader.parentTag = parentTag;
AcmeHeader.endingTag = endingTag;
AcmeHeader.defaultMJMLDefinition = defaultMJMLDefinition;

export default AcmeHeader;

But my component produce broken html, that is different from referenced "raw" html output.

For example my columns seems broken and generates with class names mj-column-per-NaN

I dig little into MJML source code, and found a bit complex logic in child elements composing, seems i need to replicate it somehow in my component? Because it's looks like that Group and/or Section requires at least proper parentWidth value, that seems missing in my custom component.

Easiest way to achieve same result as the native one is decompose my component into smaller parts, as mj-accordion does. But may be there more easy way to have proper html output from custom component?

iRyusa commented 7 years ago

Hi @ogonkov

Well this one is tricky, when developing MJML we didn't take into account that custom components can include multiple components.

When you're passing XML to MJML engine it will parse it and spit out a JSON tree before doing React stuff. The tree allows us to build the JSX to create elements. Some components need some "ghost" attributes to work ( ex parentWidth as you stated ). Thing is, when using component inside a component, the internal JSON tree isn't built properly, and we have to "rebuild it" on the fly, most of ghost attributes are lost and then it breaks.

This is really unclear and should be done with React Context but it's a bit too late to refactor it. As we're dropping React in MJML4, it will be much more viable to build components that require multiple components. We should announce the news syntax really soon now as the "usable" alpha rolls out.

In the meantime, i would suggest to use mj-include for such case, but you can try to set a width to both Wrapper & Section to see if it does the trick.

ogonkov commented 7 years ago

Thanks for explanation, @iRyusa

Setting width still didn't fix broken class for column :(

Well, let's hope that making custom elements would be more clear in next version.

iRyusa commented 7 years ago

Updating title for clarity

New API is available in MJML 4 first "ready to use" alpha if you want to give it a try @ogonkov

ogonkov commented 7 years ago

How to migrate to MJML4? I've tired once but my templates compiles with empty <body>

iRyusa commented 7 years ago

Hi @ogonkov Everything you need to know is here : https://medium.com/mjml-making-responsive-email-easy/the-first-alpha-for-mjml-4-is-here-bde5fbd3f316 and here https://github.com/mjmlio/mjml/releases/tag/4.0.0-alpha.3

ogonkov commented 7 years ago

Seems it works, but with a lot of warnings like

Warning: .mjmlconfig file ./dist/FooComponent/FooComponent.js opened from /home/vagrant/project/app/emails-mjml has an error : TypeError: (0 , _mjmlCore.MJMLElement) is not a function
iRyusa commented 7 years ago

.mjmlconfig isn't supported yet in MJML4 alpha are you sure you're running the last alpha ?

ogonkov commented 7 years ago

Yes, i have remove previous version of mjml from package.json, remove node_modules and install the next one

iRyusa commented 6 years ago

Hi ! A new API has been introduce in MJML4 to fix this issue (you can pass a rawXML to renderChildren https://github.com/mjmlio/mjml/releases/tag/4.0.0-beta.2)

We'll re-do our boilerplate to create custom component for the release.

Feel free to reopen if it doesn't work for you