Closed tuhlmann closed 2 years ago
@tuhlmann Happy to try to help! Could you post a little more of the React component you're trying to angularize?
@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:
In the application, the component renders, but without the children:
Is there something I'm doing wrong maybe?
Thanks, Torsten.
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
Thank you for looking into it!
@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?
@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]);
};
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. 🤞🏻
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.
@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:
For components without transcluded content, it seems to compile the rendered output of the react component, could that be, that's what the log seems to indicate?
For components with child content (which was transcluded in the original NG component, it shows the following error (first line is the content with the substituted variables- and this is also showing on the page, followed by an error:
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?
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.
@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:
That is so awesome! I'll try to find some time tomorrow to run some tests!
Thanks for your time and work!
@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!
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.
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?
No problem! I've published 17.6.2 which reverts all this.
Thank you for your time!
Hello ! Have you finally found a solution ? On a project, I have many angularized react components having angularjs children.
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.
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.
@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 :
[some angularjs code]
codes must be converted to [some react code]
if I want to use only one implementation (an-angularized-component).[some angularjs code]
codes in one time.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)
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?
Closing for now due to lack of progress/interest in working on this.
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:I would have expected to see the child contents of
my-transclude-comp
in thechildren
property of the angularized ReactMyTranscludeComp
, but thechildren
remain undefined.Am I doing something wrong? Is this case supported, and if, is there an example how to make it work?
Thanks!