I'm trying to convert some Ember codes to React. Here is what I want to transform.
From
export default Ember.Component.extend(({
didInsertElement() { }
});
To
export default class MyComponent extends React.Component {
componentDidMount() { }
}
I write a babel plugin, and try to replace the Ember.Component.extend call with an AST node which is produced by template method. Here is the code snippet.
babel-plugin
const { default: template } = require("#babel/template");
const code = `class TestComponent extends React.Component {
componentDidMount() { }
}`;
let classDeclarationNode = template.ast(code);
module.exports = function ({ types: t }) {
return {
visitor: {
CallExpression(path) {
if (!path.getSource().startsWith("Ember.Component.extend")) {
return;
}
path.replaceWith(classDeclarationNode);
}
}
};
};
Output
export default (function () {
class TestComponent extends React.Component {
componentDidMount() {}
}
})();
Instead of the expected code I wrote above, I get the ClassDeclaration statement surrounded with IIFE. Is there any way to remove the IIFE?
I have struggled with the problem one whole day, but have no way to resolve it.
BTW, I also tried parseExpression method, but still can't get what exactly I want.
babel-plugin
const { parseExpression } = require('#babel/parser');
const code = `class TestComponent extends React.Component {
componentDidMount() { }
}`;
let expression = parseExpression(code);
module.exports = function ({ types: t }) {
return {
visitor: {
CallExpression(path) {
if (!path.getSource().startsWith("Ember.Component.extend")) {
return;
}
path.replaceWith(expression);
}
}
};
};
Output
export default (class TestComponent extends React.Component {
componentDidMount() {}
});
It's quite close to the correct code, except an extra pair of (). Is there any way to generate the pure class declaration?
Thanks for the help of loganfsmyth on Slack, the problem was finally resolved. I should replace the node of whole export default but not only the CallExpression. Here is the code.
babel-plugin
const { default: template } = require("#babel/template");
const code = `export default class TestComponent extends React.Component {
componentDidMount() { }
}`;
let rootNode = template.ast(code);
module.exports = function ({ types: t }) {
return {
visitor: {
ExportDefaultDeclaration(path) {
let isMatchedNode = (path.node.declaration &&
t.matchesPattern(path.node.declaration.callee, "Ember.Component.extend"));
if (!isMatchedNode) {
return;
}
path.replaceWith(rootNode);
}
}
};
};
Output
export default class TestComponent extends React.Component {
componentDidMount() {}
}
Related
React-leaflet package allows to use leaflet and react using functional components, at least all the examples at https://react-leaflet.js.org/ are using functional components. Is possible to use class components with version 3 of react-leaflet? If yes, there are examples?
It is supported to create a class components in react-leaflet v3 although not the same way as it was officially supported in previous versions.
In version v1 and v2 you were suppose to implement a custom component by:
a) extending one of the abstract classes provided by react-leaflet, for example:
class MapInfo extends MapControl {
//...
}
b) and implementing createLeafletElement (props: Object): Object method
Refer this answer for a more details since official documentation for implementing custom components for v1 and v2 versions is no longer available.
The following example demonstrates how to convert custom component into version 3.
Here is an a custom component compatible with v1/v2 version:
import React, { Component } from "react";
import { withLeaflet, MapControl } from "react-leaflet";
import L from "leaflet";
class MapInfo extends MapControl {
constructor(props, context) {
super(props);
props.leaflet.map.addEventListener("mousemove", ev => {
this.panelDiv.innerHTML = `<h2><span>Lat: ${ev.latlng.lat.toFixed(
4
)}</span> <span>Lng: ${ev.latlng.lng.toFixed(4)}</span></h2>`;
console.log(this.panelDiv.innerHTML);
});
}
createLeafletElement(opts) {
const MapInfo = L.Control.extend({
onAdd: map => {
this.panelDiv = L.DomUtil.create("div", "info");
return this.panelDiv;
}
});
return new MapInfo({ position: "bottomleft" });
}
componentDidMount() {
const { map } = this.props.leaflet;
this.leafletElement.addTo(map);
}
}
export default withLeaflet(MapInfo);
which could be converted into the following class component compatible with version 3:
class MapInfo extends React.Component {
createControl() {
const MapInfo = L.Control.extend({
onAdd: (map) => {
const panelDiv = L.DomUtil.create("div", "info");
map.addEventListener("mousemove", (ev) => {
panelDiv.innerHTML = `<h2><span>Lat: ${ev.latlng.lat.toFixed(4)}</span> <span>Lng: ${ev.latlng.lng.toFixed(4)}</span></h2>`;
console.log(panelDiv.innerHTML);
});
return panelDiv;
},
});
return new MapInfo({ position: "bottomleft" });
}
componentDidMount() {
const {map} = this.props;
const control = this.createControl();
control.addTo(map);
}
render() {
return null;
}
}
function withMap(Component) {
return function WrappedComponent(props) {
const map = useMap();
return <Component {...props} map={map} />;
};
}
export default withMap(MapInfo);
Note: withMap is a high order component which is intended to pass map instance into class component
Here is a demo
I have this code in my afterEach block that works to console log the page source for a failed spec. But I want to move it to another class instead.
afterEach(function () {
const state = this.currentTest.state;
if (state === 'failed') {
browser.driver.getPageSource().then(function (res) {
console.log(res);
});
}
});
but when i try to use the following in another class, i get 'Property 'currentTest' does not exist on type 'HelperClass'. How do i declare the currentTest property?
import { browser, } from 'protractor';
export class HelperClass {
public getSource() {
const state = this.currentTest.state;
if (state === 'failed') {
browser.driver.getPageSource().then(function (res) {
console.log(res);
});
}
}
}
Try like below
test file:
const helper = new HelperClass(); // create object for class
afterEach(async ()=> {
const state = this.currentTest.state;
await helper.getSource(state);
});
Class File
import { browser, } from 'protractor';
export class HelperClass {
public getSource(state:any) {
if (state === 'failed') {
browser.driver.getPageSource().then(function (res) {
console.log(res);
});
}
}
}
Here we are sending the State from your test file.
Hope it helps you
Given that there is not much examples about this, I am following the docs as best as I can, but the validation is not reactive.
I declare a schema :
import { Tracker } from 'meteor/tracker';
import SimpleSchema from 'simpl-schema';
export const modelSchema = new SimpleSchema({
foo: {
type: String,
custom() {
setTimeout(() => {
this.addValidationErrors([{ name: 'foo', type: 'notUnique' }]);
}, 100); // simulate async
return false;
}
}
}, {
tracker: Tracker
});
then I use this schema in my component :
export default class InventoryItemForm extends TrackerReact(Component) {
constructor(props) {
super(props);
this.validation = modelSchema.newContext();
this.state = {
isValid: this.validation.isValid()
};
}
...
render() {
...
const errors = this.validation._validationErrors;
return (
...
)
}
}
So, whenever I try to validate foo, the asynchronous' custom function is called, and the proper addValidationErrors function is called, but the component is never re-rendered when this.validation.isValid() is supposed to be false.
What am I missing?
There are actually two errors in your code. Firstly this.addValidationErrors cannot be used asynchronously inside custom validation, as it does not refer to the correct validation context. Secondly, TrackerReact only registers reactive data sources (such as .isValid) inside the render function, so it's not sufficient to only access _validationErrors in it. Thus to get it working you need to use a named validation context, and call isValid in the render function (or some other function called by it) like this:
in the validation
custom() {
setTimeout(() => {
modelSchema.namedContext().addValidationErrors([
{ name: 'foo', type: 'notUnique' }
]);
}, 100);
}
the component
export default class InventoryItemForm extends TrackerReact(Component) {
constructor(props) {
super(props);
this.validation = modelSchema.namedContext();
}
render() {
let errors = [];
if (!this.validation.isValid()) {
errors = this.validation._validationErrors;
}
return (
...
)
}
}
See more about asynchronous validation here.
new to Meteor and running into this issue. I am using Meteor 1.3.3
When I try to pass props from my parent Container to my React Component it keeps throwing an error I will post below.
Here is my React component Prospect.jsx:
import React from 'react'
import { createContainer } from 'meteor/react-meteor-data'
import { Residents } from '/collections/residents.jsx'
import ReactDOM from 'react-dom';
import RaisedButton from 'material-ui/RaisedButton';
// import '/collections/residents.jsx'
class Prospect extends React.Component {
render() {
return(
<div>
<h1>Prospect Resident - {this.props.prospect.name.first} </h1>
<RaisedButton label="Default" />
</div>
)
}
}
Prospect.propTypes = {
// prospect: React.PropTypes.object
}
export default createContainer((params) => {
const paramsId = params.params.prospectId
Meteor.subscribe('residents');
// Meteor.subscribe('resident');
prospect = Residents.find({_id: paramsId}).fetch()
console.log(prospect[0])
return {
prospect: prospect
}
}, Prospect)
and here is my Mongo collection
residents.jsx
import { Mongo } from 'meteor/mongo'
export const Residents = new Mongo.Collection('residents')
const nameSchema = new SimpleSchema({
first: {type: String},
last: {type: String}
})
const residentSchema = new SimpleSchema({
cId: { type: String },
name: { type: nameSchema },
status: { type: String },
})
Residents.attachSchema(residentSchema)
// METHODS
Meteor.methods({
'residents.insert'(resident) {
Residents.insert(resident)
}
})
// PUBLICATIONS
if(Meteor.isServer) {
Meteor.publish('residents', function() {
return Residents.find()
})
Meteor.publish('resident', function(id) {
return Residents.find({_id: id})
})
}
and here is my Route
FlowRouter.route('/prospects/:prospectId}', {
name: 'prospectShow',
action(params) {
mount(LoggedIn, { content:
<MuiThemeProvider muiTheme={getMuiTheme()}>
<Prospect params={{prospectId: params.prospectId}} />
</MuiThemeProvider>
})
}
So when I go to localhost:3000 route I get the error
Prospect.jsx:14Uncaught TypeError: Cannot read property 'name' of undefined
Exception from Tracker recompute function:
debug.js:41 TypeError: Cannot read property '_currentElement' of null
at ReactCompositeComponentWrapper._updateRenderedComponent (ReactCompositeComponent.js:772)
at ReactCompositeComponentWrapper._performComponentUpdate (ReactCompositeComponent.js:753)
at ReactCompositeComponentWrapper.updateComponent (ReactCompositeComponent.js:672)
at ReactCompositeComponentWrapper.receiveComponent (ReactCompositeComponent.js:571)
at Object.receiveComponent (ReactReconciler.js:127)
at ReactCompositeComponentWrapper._updateRenderedComponent (ReactCompositeComponent.js:775)
at ReactCompositeComponentWrapper._performComponentUpdate (ReactCompositeComponent.js:753)
at ReactCompositeComponentWrapper.updateComponent (ReactCompositeComponent.js:672)
at ReactCompositeComponentWrapper.performUpdateIfNecessary (ReactCompositeComponent.js:585)
at Object.performUpdateIfNecessary (ReactReconciler.js:160)
My console.log(prospect[0]) in the container returns the object just fine, and it also works if I pass it in like this
return {
prospect: {name: {first: 'Joe', last: 'Smith'}}
}
So it's something about the returned object I think. Any help would be greatly appreciated, thanks
I ended up going with a solution like this. If anyone wants to answer and explain why this is needed (I thought in meteor 1.3 this wasn't needed anymore) I will accept your answer.
import React from 'react'
import { createContainer } from 'meteor/react-meteor-data'
import { Residents } from '/collections/residents.jsx'
class Prospect extends React.Component {
render() {
if(!this.props.ready){return <span>Loading...</span>}
const { prospect } = this.props
return(
<div>
<h1>{prospect.name.first} {prospect.name.last}</h1>
<div>Company: <b>{prospect.cId}</b></div>
</div>
)
}
}
Prospect.propTypes = {
ready: React.PropTypes.bool.isRequired,
prospect: React.PropTypes.object.isRequired
}
export default createContainer((params) => {
return {
ready: Meteor.subscribe('resident', params.id).ready(),
prospect: Residents.findOne(params.id)
}
}, Prospect)
I want to modify the class so that it does not use the ApplicationRef. In other words how to get hold of main app not using app ref.
#Injectable()
export class ToastsManager {
container: ComponentRef<any>;
private options = {
autoDismiss: true,
toastLife: 1000
};
private index = 0;
container: ComponentRef<any>;
private options = {
autoDismiss: true,
toastLife: 1000
};
private index = 0;
constructor(private resolver: ComponentResolver,
private appRef: ApplicationRef,
#Optional() #Inject(ToastOptions) options) {
if (options) {
Object.assign(this.options, options);
}
}
show(toast: Toast) {
if (!this.container) {
// a hack to get app element in shadow dom
let appElement: ViewContainerRef = new ViewContainerRef_(this.appRef['_rootComponents'][0]._hostElement);
this.resolver.resolveComponent(ToastContainer)
.then((factory: ComponentFactory<any>) => {
this.container = appElement.createComponent(factory);
this.setupToast(toast);
});
} else {
this.setupToast(toast);
}
}
I try with the #ViewChild but it does not work.
You could do with ApplicationRef what Brandon Roberts demonstrates in https://github.com/angular/angular/issues/4112#issuecomment-139381970 to get a reference to the Router in CanActivate().
Probably better would be a shared service
#Injectable()
export class Shared {
appRef = new BehaviorSubject();
setAppRef(appRef:ApplicationRef) {
this.appRef.emit(appRef);
}
}
export class ToastsManager {
constructor(private resolver: ComponentResolver,
private appRef: ApplicationRef,
shared:Shared,
#Optional() #Inject(ToastOptions) options) {
shared.setAppRef(appRef);
}
}
export class OtherClassThatNeedsAppRef {
constructor(shared:Shared) {
shared.appRef.subscribe(appRef => this.appRef = appRef);
}
}