atomicojs / atomico

Atomico a micro-library for creating webcomponents using only functions, hooks and virtual-dom.
https://atomicojs.dev
MIT License
1.16k stars 43 forks source link

How to expose props and observable props? #27

Closed kristianmandrup closed 3 years ago

kristianmandrup commented 4 years ago

Hi,

I'm trying to use atomico with frintjs, ie to allow easy rendering atomico components as Micro Frontends. Here is my repo frint-atomico

The contract is as follows:

export default {
  render,
  hydrate,
  streamProps,
  isObservable,

  getMountableComponent,
  observe,
  Region,
  Provider,

  RegionService,

  ReactHandler,
};

render is the render function, naturally, so I would expect I just need to export a function that calls html<component name> or?

streamProps is a stream of the props? as far I can see, the props are not currently exposed in atomico? I guess I could tweak atomico or define a wrapper or sth?

hydrate is for SSR and not important at this point.

observe is ` function to observe the component props I believe.

Region is a class responsible for drawing the component in a FrintJS region and communicating props to/from the app. Here is the vue example.

import composeHandlers from 'frint-component-utils/lib/composeHandlers';
import RegionHandler from 'frint-component-handlers/lib/RegionHandler';

export default {
  name: 'Region',
  inject: ['app'],
  props: [
    'name',
    'uniqueKey',
    'data',
  ],
  beforeCreate() {
    this._handler = composeHandlers(
      VueHandler,
      RegionHandler,
      {
        component: this,
      },
    );
  },
  data() {
    return this._handler.getInitialData();
  },
  updated() {
    this._handler.afterUpdate();
  },
  beforeMount() {
    this._handler.app = this.app; // context is resolved only now
    this._handler.beforeMount();
  },
  beforeDestroy() {
    this._handler.beforeDestroy();
  },
  render(h) { // eslint-disable-line
    if (this.listForRendering.length === 0) {
      return null;
    }

    return (
      <div>
        {this.listForRendering.map((item) => {
          const { Component, name } = item; // eslint-disable-line
          return (
            <Component key={`app-${name}`} />
          );
        })}
      </div>
    );
  }
};

listForRendering is the list of apps (components) to be rendered in the region. The life cycle methods are not essential, but beforeMount could perhaps be linked to the custom element internal connectedCallback method?

Is it possible to do sth like the following to dynamically render the right app component by name?

html`<app-${name}></app-${name}>`

I will link this issue on an issue in the frint repo to get some advice from that end as well.

I wonder if I can do sth like this:

import html from "atomico/html";
import { customElement } from "atomico";
import { from } from 'rxjs';

const MyTag = (props) => html`<h1>Hi! ${props.value}</h1>`;

const MyTagComponent = (props) => ({
  render(props) => MyTag(props),
  props,
  props$: from(props)
}

MyTag.props = {
  value: { type: String, value: "Atomico" }
};

customElement("my-tag", MyTag);

Cheers ;)

UpperCod commented 4 years ago

@kristianmandrup I apologize for the delay in my response

Is it possible to do sth like the following to dynamically render the right app component by name? html<app-${name}></app-${name}>.

Atomico support template-string thanks to htm, this does not support this type of syntax, but something similar can be achieved as well, example.

import { h, customElement } from "atomico";
import html from "atomico/html";

function Tag(name) {
  return props => h(name, props);
}

const Greeting = () => {
  return html`
    <host>
      <${Tag("h1")}>
       ...h1
      </>
      <${Tag(
        "img"
      )}  src="https://avatars2.githubusercontent.com/u/43358768?s=400&v=4"/>
    </host>
  `;
};

customElement("custom-element", Greeting);
UpperCod commented 4 years ago

I read the documentation of frint to better understand its purpose, as I understand this library is that it creates an environment for microfontend focused on injection over predisposed containers (regions), These regions are encapsulated by a customRender that facilitates communication with the context created with frint.

This is good for communicating applications away from the use of webcomponents, I personally find frint complex as a micrfrontend solution, what would you think of this syntax?

import { h, customElement } from "atomico";
import createContainer from "@atomico/microfrontend/create-container";
import renderReact from "@atomico/microfrontend/render-react";

let ComponentReact = createContainer(
  () => import("htts://localhost:8010/header.js"),
  renderReact
);

//Component loading
function Loading() {
  return "...loading!";
}

function MyApp() {
  // handler that allows communication, to generate any effect
  function handler(message) {
    console.log(message);
  }
  return (
    <host>
      react container!
      <ComponentReact loading={<Loading />} anyHandler={handler} />
    </host>
  );
}
kristianmandrup commented 4 years ago

Thank you so much for getting back to me with some good examples and suggestions. I've communicated briefly with the author of FrintJS and he suggested I had a look at his latest framework ProppyJS which looks simpler and more flexible.

I like the syntax you proposed in your latest example. I'm looking for a good Micro Frontend solution based on a very light tech stack, leveraging modern built-in browser/JS technology as much as possible and having components or services subscribe to Data Streams and having responsibility for distinct part of UI (sink) similar to a modern backend (micro services subscribing to Kafka Event Stream and maintaining own data source - sink)

I'll give it a try. Cheers!

UpperCod commented 4 years ago

Thank you for considering Atomico to manage a microfrontend environment, I have analyzed the shared library, I think that the composition with jsx or template string has more potential, since it creates a context by instance and not by module, eg:

Example counter proppyJs

This is one context per module, the problem with this type of context is that it is not configurable by composition.

https://proppyjs.com/docs/examples/react-counter/

import React from 'react';
import { compose, withStateHandlers, shouldUpdate } from 'proppy';
import { attach } from 'proppy-react';

const P = compose(
  withStateHandlers(
    { counter: 0 },
    { handleIncrement: props => () => ({ counter: props.counter + 1 }) }
  ),
  shouldUpdate((prevProps, nextProps) => nextProps.counter % 2 === 0)
);

function MyComponent({ counter, handleIncrement }) {
  return (
    <div>
      <p>Counter: {counter}</p>

      <button onClick={handleIncrement}>
        Increment
      </button>
    </div>
  );
}

export default attach(P)(MyComponent);

My object with Atomico would be to achieve a syntax like this

import { h, customElement, useProp } from "atomico";
import createContainer from "@atomico/microfrontend/create-container";
import renderReact from "@atomico/microfrontend/render-react";
import renderVue from "@atomico/microfrontend/render-vue";

/**
 * createContainer creates a container as a web-component, for the given render.
 */
let ComponentReact = createContainer(() => import("..anyUrl"), renderReact);
let ComponentVue = createContainer(() => import("..anyUrl"), renderVue);

function MyComponent() {
  let [count, setCount] = useProp("count");

  let increment = () => setCount(count + 1);
  let decrement = () => setCount(count - 1);

  return (
    <host>
      This is a root component with Atomico, it creates an independent state by
             instance, this state is shared with the react and vue component
      <section>
        This is a component with React.
        <ComponentReact
          handlerIncrement={increment}
          handlerDecrement={decrement}
          count={count}
        ></ComponentReact>
      </section>
      <section>
        This is a component with Vue.
        <ComponentVue
          handlerIncrement={increment}
          handlerDecrement={decrement}
          count={count}
        ></ComponentVue>
      </section>
    </host>
  );
}

MyComponent.props = {
  count: {
    type: Number,
    reflect: true,
    value: 0,
    event: { type: "counterChange" }
  }
};

customElement("my-component", MyComponent);

Note that the component Component Vue receives as variables the redefined variables, this allows the code of the component created with the library, should not have Refactoring...

I share the progress of the package, this is in process, some renders like VUE need to be added

To achieve @atomico/microfrontend, I have been refactoring add-ons that work in parallel as bundle-cli and the same Atomico core

kristianmandrup commented 4 years ago

Very cool 😎