xjpro / react-in-angularjs

A super simple way to render React components in AngularJS
The Unlicense
23 stars 13 forks source link

Angularize component with ng-transclude #11

Closed tuhlmann closed 2 years ago

tuhlmann commented 2 years ago

Hi,

first of all, thanks for this great helper tool to port components to React!

I'm trying to port a component that uses the ng-transclude directive, that means, the call of this component in the angular template that uses it looks something like this:

<my-transclude-comp name="value" ...>
  <div>
    ...some standard non angular markup here ...
  </div>
</my-transclude-comp>

I would have expected to see the child contents of my-transclude-comp in the children property of the angularized React MyTranscludeComp, but the children remain undefined.

Am I doing something wrong? Is this case supported, and if, is there an example how to make it work?

Thanks!

xjpro commented 2 years ago

@tuhlmann Happy to try to help! Could you post a little more of the React component you're trying to angularize?

tuhlmann commented 2 years ago

@xjpro Thanks for your support!

Let me try to describe my environment first. Our web app uses:

I created a minimal example that can reproduce the problem. The angularized React component is called from an Angular template like so:

<div class="text-input flex">
  <inline-edit-test value="'Hans'">
    <span>Harry</span>
  </inline-edit-test>
</div>

This is the React component is this:

import { angularize } from 'react-in-angularjs'
import angular from 'angular'

interface Props {
  value: string
  children?: React.ReactNode
}

export function InlineEditTest(props: Props): JSX.Element {
  console.log('InlineEditTest', props)
  console.log('InlineEditTest children', props.children)

  const { value } = props

  return (
    <div>
      <p>Inline-Edit, children are:</p>
      <div>{props.children}</div>
    </div>
  )
}

angularize(InlineEditTest, 'inlineEditTest', angular.module('project-pad'), {
  value: '<',
})

The output I see in the console for these two log statements:

image

In the application, the component renders, but without the children:

image

Is there something I'm doing wrong maybe?

Thanks, Torsten.

xjpro commented 2 years ago

Very interesting.

Currently react-in-angularjs does not actually send children, so that's why you're not seeing them in your React code. I'm going to try to see if it's possible to send pass them along to React.createElement https://github.com/xjpro/react-in-angularjs/blob/master/src/index.js#L44

tuhlmann commented 2 years ago

Thank you for looking into it!

xjpro commented 2 years ago

@tuhlmann I've added some ability to pass along children in version 17.6.1. Can you try this version and let me know if it meets your needs?

tuhlmann commented 2 years ago

@xjpro That's awesome, thank you! I see any child elements given to the angularized component! For one use case that is totally sufficient.

In one other case there are some interpolations and ng-show statements as part of the child elements, like for instance:

<inline-edit-test value="'Hans'">
  <span ng-show="someConditionMet">Harry {{$ctrl.lastName}}</span>
</inline-edit-test>

Is there a way to get the html through the Angular rendering process before making it a React component? Would it work to pass the html through $compile(childElements)($scope) ?

Thanks!

Update: I did hack a const compiled = $compile($element[0].innerHTML)($scope); in the angularize onChange function. But it's output didn't contain a value from scope that I added to the child html. I might have done something wrong though.

Here's my code (I added $compile in to the controller def:

this.$onChanges = () => {
  const compiled = $compile($element[0].innerHTML)($scope);
  console.log("compiled", compiled[0]);
  const children = ReactHtmlParser(compiled[0]);
  ReactDOM.render(React.createElement(Component, this, ...children), $element[0]);
};
xjpro commented 2 years ago

It would be very convenient to figure out a way to do that but none of my attempts have ever worked. You've gotten as far along with it as I have. For my project I've slowly converted everything from the bottom up so that no child AngularJS components were needed. Still, it would be nice if we could find a way to $compile the AngularJS as it would make conversions a lot smoother. If it worked then you'd be able to replace AngularJS components with React anywhere in the tree.

I would amend your updated $onChanges to be

this.$onChanges = () => {
  const compiled = $compile($element[0].innerHTML)(this.$scope);
  const children = ReactHtmlParser(compiled[0]?.outerHTML || "");
  ReactDOM.render(
    React.createElement(Component, this, ...children),
    $element[0]
  );
};

This works in the sense that it shows the children again, but what I'm finding is none of the AngularJS directives seem to actually run. I'm not sure why that would be as all examples I can find of $compile online seem to work with strings. I'm still looking at it, but maybe this will push you into a solution. 🤞🏻

tuhlmann commented 2 years ago

Thanks @xjpro !

Please don't spend too much time on it. I just have only a few more ng-transclude to convert, and even if that doesn't work, the lib is tremendously helpful!

On the contrary, I'm not sure if the additional dependency (ReactHtmlParser, and I also had to install Buffer) is worth having if children are not fully supported?

I'll do a bit more research over the weekend and see if I can find a way to make it work, or a pointer why it doesn't.

tuhlmann commented 2 years ago

@xjpro This seems to be (partially) working:

        this.$onChanges = () => {
          const compiled = $compile($element[0].innerHTML)(this.$scope).html() || "";
          const interpolated = $interpolate(compiled)(this.$scope);
          if (interpolated?.length) {
            console.log("compiled", compiled, interpolated, this.$scope);
          }
          const children = ReactHtmlParser(interpolated || "");
          ReactDOM.render(
            React.createElement(Component, this, ...children),
            $element[0]
          );
        };

Adding the $interpolated steps does substitute the variables. But, I see the following two problems:

Bildschirmfoto vom 2022-02-20 15-48-14

Please note that this error also occurs in your version that just contains the line const children = ReactHtmlParser($element[0].innerHTML);.

Any ideas how to fix that one?

xjpro commented 2 years ago

This is fantastic. I'm hopeful we can come to some sort of solution, although I too am finding it troublesome to work out exactly what steps need to be taken. I think interpolate should come before compile though, this seems to work somewhat

this.$onChanges = () => {
  const interpolated = $interpolate($element[0].innerHTML)(this.$scope);
  const compiled = $compile(interpolated)(this.$scope);
  const children = ReactHtmlParser(compiled[0]?.outerHTML || "");
  ReactDOM.render(
    React.createElement(Component, this, ...children),
    $element[0]
  );
};

What troubles me about this is from my reading of the documentation $compile is supposed to do interpolation internally, but for some reason isn't happening.

xjpro commented 2 years ago

@tuhlmann Major breakthrough!

It turns out you have to $scope.$apply() in order to get html to convert. This is getting very close

this.$onChanges = () => {
  // Prepare children
  const compiled = $compile($element[0].innerHTML)(this.$scope);
  this.$scope.$apply(); // <--- new step
  const children = ReactHtmlParser(compiled[0]?.outerHTML || "");

  ReactDOM.render(
    React.createElement(Component, this, ...children),
    $element[0]
  );
};

Note that lack of interpolate. I think AngularJS does that within $compile.

Issues:

tuhlmann commented 2 years ago

That is so awesome! I'll try to find some time tomorrow to run some tests!

Thanks for your time and work!

tuhlmann commented 2 years ago

@xjpro Here is a version that works for me:

        this.$onChanges = () => {
          // Prepare children
          $timeout(() => {
            let children = undefined;
            if (this?.$scope) {
              const compiled = $compile($element[0].innerHTML)(this.$scope);
              this.$scope.$apply(); // <--- new step
              children = ReactHtmlParser(compiled[0]?.outerHTML || "");
              if (children?.length) {
                console.log("children", compiled[0]?.outerHTML || "", children, Component);
              }
            }

            ReactDOM.render(
              React.createElement(Component, this, ...(children || [])),
              $element[0]
            );
          })
        };

No error anymore! I had to wrap it in $timeout because it would complain about already being in a digest cycle when doing the this.$scope.$apply.

I have one component that behaves strange though. It doesn't have children but the console.log will log the html output of that React component as children of it. But it doesn't produce an error or strange rendering because this component in question does not define any children, so it will not render the duplication. But, when I define this component with children, and render them at the end of the existing markup, I see the same markup twice!

It seems this component is angularized after being reactified. Very strange. But I think this is not a problem with the algorithm. I might have made a mistake with that particular component.

But the goal of this PR should be reached!

Thank you!

xjpro commented 2 years ago

I tried something similar (you don't need to $apply at all if you do $timeout), but the problem is this seems to cause the digest to never "stabilize". You can see this is if you add a console log in the $onChanges function: it gets continuously called as if there are always changes, forever.

tuhlmann commented 2 years ago

Hmm, the current solution doesn't work for me, unfortunately. Since I currently have mitigated the need for transclusion, maybe you should stash the changes you made into a branch, until we can figure out a way to make it work in all scenarios?

xjpro commented 2 years ago

No problem! I've published 17.6.2 which reverts all this.

tuhlmann commented 2 years ago

Thank you for your time!

pdoreau commented 2 years ago

Hello ! Have you finally found a solution ? On a project, I have many angularized react components having angularjs children.

tuhlmann commented 2 years ago

I don't think the solution we had worked on would work in your scenario. All it did was to support the ng-transclude directive. In my project I have abondoned the strategy of using ng-transclude with reactified NG components and instead specialized the react components, maybe keeping the React and NG version side by side until everything could be ported.

The solution however to my understanding does not support NG children from React components. There might be ways to get this to work, but I never used that strategy. We started at the bottom and reactified the small parts, and thanks to this project we could simply use them from the NG parents as they were before. Then we worked our way up and finally were able to remove the ng-react glue from the components that are now only used from within the React tree.

Does that help?

Torsten.

xjpro commented 2 years ago

What @tuhlmann suggests is how I've handled it in my project as well. Work from the bottom up, ie no AngularJS children of a React component. So I converted AngularJS components into React then use react-in-angularjs to use those components in AngularJS parents until finally react-in-angularjs isn't needed anymore.

It would be nice to allow the React code to use AngularJS components but to date we have yet to come up with a strategy that both 1) renders the HTML and 2) updates appropriately on state changes.

pdoreau commented 2 years ago

@tuhlmann Not really. Consider the following snippet, which is my current state

<a-classic-angularjs-component>
   [some angularjs code]
</a-classic-angularjs-component>

The goal, at the end is to end up with

<an-angularized-component>
   [some react code]
<an-angularized-component>

The problem is that's it's apparently impossible to add this intermediate step

<an-angularized-component>
  [some angularjs code]
<an-angularized-component>

Which means 2 painful consequences :

Do you have any other suggestions ?

@xjpro I see. The problem is that If I add a new content which requires the component above, this must be handled by angularjs, (unless I maintain 2 versions)

xjpro commented 2 years ago

This limitation is covered in the "caveats" section of the README. We've gotten close a few times using $compile but the issue came down to knowing when to update without blowing up performance. You can find the latest attempts above in this very issue. The code that makes react-in-angularjs work is actually very small, perhaps you could take a crack at making it work the way you would like to @pdoreau?

xjpro commented 2 years ago

Closing for now due to lack of progress/interest in working on this.