In my react app, I have an onClick function that isn't being recognized (TypeError: _this2.click is not a function) when called from dynamically-generated components. I poked around for issues with functions not being bound correctly, but they seem to be. Here's the code:
class C extends React.Component {
constructor(props) {
super(props);
// Bind components
this.eventComponent = this.eventComponent.bind(this);
this.click = this.click(this);
}
/**
* Click function for when a user selects their choice
* #param {[int]} id [id of the event the user is selecting]
*/
click(id) {
console.log(id)
}
/**
* Draws an event component (dynamically generated)
* #param {[String]} name [name of the event]
* #param {[String]} summary [summary of event]
* #return {[Object]} [react element of an event]
*/
eventComponent(name, summary, id) {
if (name != null && summary != null) {
return (
<div >
<h1>{name}</h1>
<p>{summary}</p>
<button onClick={() => this.click(id)}>Here is a button!</button>
</div>
);
}
}
render() {
var event = this.state.event
var objArray = this.state.objArray
var eventMap;
if (event) {
eventMap = objArray.map(function(event) {
// Get first property
var firstProp;
var k;
for(var key in event) {
if(event.hasOwnProperty(key)) {
firstProp = event[key];
k = key;
break;
}
}
return this.eventComponent(firstProp.title, firstProp.summary, k);
}.bind(this))
} else {
eventMap = <p>No events found!</p>;
}
// Generate a default HTML object
var eventComponent = (
<div>
{eventMap}
</div>
);
return eventComponent;
}
}
in your constructor correct this this.click = this.click(this);
to this.click = this.click.bind(this);
The most easy and convenient way is to use arrow functions, so you don't need to do binding in constructor anymore, a lot easier, isn't it?
so just remove this from constructor:
this.click = this.click.bind(this);
and change your function to:
click = (id) => {
console.log(id)
}
As answered by Vikas,
either you can follow that approach or you can use arrow syntax for functions using which there will be no need to bind functions.
Eg.
Click = (Id) => {
} .
Related
Hello everyone I tried getting react-hook-form to work with KotlinJS using Dukat and Wrappers, this is what I got.
(the wrapper works fine, tried logging it in the console and it indeed is useForm from the library)
This is how I try to use the useForm hook
val useForm = useForm<ProfileModel>()
val register = useForm.register
val errors = useForm.formState.errors
val handleSubmit = useForm.handleSubmit
val registerInstagram = register("instagram", object : validateOptions<ProfileModel, Any> {
override var required = true
override var maxLength = 40
})
form(classes = "") {
attrs.onSubmit = {
it.preventDefault()
console.log("Submit")
handleSubmit({ data, ev ->
console.log("On Submit " + data)
}, { errors, ev ->
console.log("On submit error " + errors)
})
}
input(type = InputType.text,
classes = "$defaultInputStyle mb-4") {
attrs.placeholder = "Name"
register("name", object : validateOptions<ProfileModel, Any> {
override var required = true
override var maxLength = 15
})
}
input(type = InputType.text,
classes = "$defaultInputStyle mb-4") {
this.ref = registerInstagram.ref
attrs {
this.placeholder = "Instagram"
this.onChange = registerInstagram.onChange
this.onBlur = registerInstagram.onBlur
this.name = registerInstagram.name
}
}
button(classes = defaultButtonStyle, type = ButtonType.submit) {
+"Submit"
}
}
}
useForm indeed does work and it gives me all of the right values like register, errors and such but I am unable to properly connect it to the Input element in Kotlinjs.
Register returns the following, I edited the interface to fit into the attributes of React DOM, as in regular react form hook you'd use <Input (...register) which would just apply all of these 4 things to the element.
external interface UseFormRegisterReturn {
var onChange: ChangeEventHandler<*> // I changed this
var onBlur: FocusEventHandler<*> // I changed this
var ref: RefCallback<*> //I changed this
var name: InternalFieldName
}
When I click submit I get the submit printed but the handleSubmit from the useForm isn't ran and no data is getting printed. Not sure what else can I do with this setup.
Here's the rest of the relevant Wrapper code
#JsName("useForm")
external fun <TFieldValues : FieldValues> useForm(props: `T$23`<TFieldValues> = definedExternally): UseFormReturn<TFieldValues, Any>
external interface UseFormReturn<TFieldValues : FieldValues, TContext : Any?> {
var watch: UseFormWatch<TFieldValues>
var getValues: UseFormGetValues<TFieldValues>
var setError: UseFormSetError<TFieldValues>
var clearErrors: UseFormClearErrors<TFieldValues>
var setValue: UseFormSetValue<TFieldValues>
var trigger: UseFormTrigger<TFieldValues>
var formState: FormState<TFieldValues>
var reset: UseFormReset<TFieldValues>
var handleSubmit: UseFormHandleSubmit<TFieldValues>
var unregister: UseFormUnregister<TFieldValues>
var control: Control<TFieldValues, TContext>
var register: UseFormRegister<TFieldValues>
var setFocus: UseFormSetFocus<TFieldValues>
}
Basically, I am wondering how can I register the useForm to my elements.
EDIT:
Some things you have to do
div("w-screen h-screen items-center justify-center p-4") {
form(classes = "") {
attrs.onSubmit = {
it.preventDefault() 1)
handleSubmit({ data, ev ->
console.log("On Submit ", data)
}, { errors, ev ->
console.log("On submit error ", errors)
})(it) 2)
}
input(type = InputType.text,
classes = "$defaultInputStyle mb-4") {
this.ref = registerInstagram.ref
attrs { 3)
this.placeholder = "Instagram"
this.onChange = registerInstagram.onChange
this.onBlur = registerInstagram.onBlur
this.name = registerInstagram.name
}
}
button(classes = defaultButtonStyle, type = ButtonType.submit) {
+"Submit"
}
Prevent default (if you don't want page refresh)
the function handleSubmit RETURNS A FUNCTION that needs an event as its parameter. This was missing for me, you need to call the handleSubmit()()
Apply all of the stuff returned from the register function, onBlur, name onChange and ref
This at least sends the data, but validation and errors aren't still happening
Okay, here's the full answer with working errors and everything, hopefully this will help someone.
val useForm = useForm<ProfileModel>()
val register = useForm.register
val errors = useForm.formState.errors.asDynamic()
val handleSubmit = useForm.handleSubmit
val registerName = register("name", js("{required: 'Name is required', maxLength: {" +
"value: maxNameLength , message:'Max length for a name is ' + maxNameLength}}"))
div("w-screen h-screen items-center justify-center p-4") {
form(classes = "") {
attrs.onSubmit = {
it.preventDefault()
handleSubmit({ data, ev ->
console.log("On Submit ", data)
}, { errors, ev ->
console.log("On submit error ", errors)
})(it)
}
input(type = InputType.text,
classes = "$defaultInputStyle mb-4") {
this.ref = registerName.ref
attrs {
this.placeholder = "Name"
this.onChange = registerName.onChange
this.onBlur = registerName.onBlur
this.name = "name"
}
}
if(jsTypeOf(errors.name) !== "undefined" ) {
val text = errors.name.message.toString()
p("ml-3 text-sm text-red-600 -mt-2 mb-2") {
+text
}
}
input(classes = defaultButtonStyle, type = InputType.submit) {
}
Prevent default (if you don't want page refresh)
the function handleSubmit RETURNS A FUNCTION that needs an event as its parameter. This was missing for me, you need to call the handleSubmit()()
Apply all of the stuff returned from the register function, onBlur, name onChange and ref
To get errors to work, I had to cast errors to Dynamic, and for error messages I had to check if the error exists in the first place for that field, if it does show the error p. One issue I encountered is that you cant directly use errors.name.message but I had to add it to a variable and cast it to a string.
For validation I changed the second parameter of register to dynamic and now I just pass js({whatever options I need}) no autocomplete and such but the JS compiler does check for mistakes while writing and it's pretty simple to write out so shouldnt pose a problem.
EDIT: Only thing currently missing is when using this library with Typescript it will warn you that you're missing some fields in the IDE when using useForm<Type
Adding a node to a list and yet a component is not re-rendering. Mobx Chrome Extension dev tools says it's a dependency but for some reason still no reaction!
A button renders 'Add' or 'Remove' based on whether a node is in a list. It doesn't re-render unless I move to another component and then open this component again.
Buttons:
#inject("appStore") #observer
class EntityTab extends Component {
...
render() {
return (
...
{/* BUTTONS */}
{ this.props.appStore.repo.canvas.graph.structure.dictionary[id] !== undefined ?
<div onClick={() => this.props.appStore.repo.canvas.graph.function(id)}>
<p>Remove</p>
</div>
:
<div onClick={() => this.props.appStore.currRepo.canvas.otherfunction(id)}>
<p>Add</p>
</div>
}
...
)
}
}
The Add button renders, I click on the button which triggers
this.props.appStore.currRepo.canvas.otherfunction(id)
and then we go to this function
#observable graph = new Graph();
...
#action
otherfunction = (idList) => {
// Do nothing if no ids are given
if (idList === null || (Array.isArray(idList) && idList.length === 0)) return;
// If idList is a single value, wrap it with a list
if (!Array.isArray(idList)) { idList = [idList] }
let nodesToAdd = [];
let linksToAdd = [];
// Add all new links/nodes to graph
Promise.all(idList.map((id) => { return this.getNode(id, 1) }))
.then((responses) => {
for (let i = 0; i < responses.length; i++) {
let res = responses[i];
if (res.success) {
nodesToAdd.push(...res.data.nodes);
linksToAdd.push(...res.data.links);
}
}
this.graph.addData(nodesToAdd, linksToAdd, idList, this.sidebarVisible);
});
};
The getNode function creates new Node objects from the data. For reference, those objects are instantiated as such
export default class Node {
id = '';
name = '';
type = '';
constructor(r) {
for (let property in r) {
// Set Attributes
this[property] = r[property];
}
}
}
anyway, the addToGraphFromIds triggers
this.graph.addData(nodesToAdd, linksToAdd);
and then we go to that function
#action
addData = (nodes, links) => {
this.structure.addNodes(nodes);
this.structure.addLinks(links);
};
which triggers
this.structure.addNodes(nodes);
which leads to this function
#observable map = new Map();
#observable map2 = new Map();
#observable dictionary = {};
#observable dictionary2 = {};
#observable allNodes = [];
#observable allLinks = [];
...
#action
addNodes = (nodes=[]) => {
if (!nodes || nodes.length === 0) return;
nodes = utils.toArray(nodes);
// Only consider each new node if it's not in the graph or a duplicate within the input list
nodes = _.uniqBy(nodes, (obj) => { return obj.id; });
const nodesToConsider = _.differenceBy(nodes, this.allNodes, (obj) => { return obj.id; });
// Add nodes to map
let currNode;
for (let i = 0; i < nodesToConsider.length; i++) {
currNode = nodesToConsider[i];
this.map.set(currNode.id, new Map());
this.map2.set(currNode.id, new Map());
this.dictionary[currNode.id] = currNode;
}
// Update internal list of nodes
this.allNodes = this.allNodes.concat(nodesToConsider);
};
As we can see in the first codebox,
this.props.appStore.repo.canvas.graph.structure.dictionary[id] !== undefined
Should cause the button to change values as we have added the current node. The nodes appear in the dictionary when I log or use mobx chrome extension dev tools, but I have to switch tabs and then the button will re-render. I've tried using other lines like
this.props.appStore.repo.canvas.graph.structure.allNodes.includes(node)
but that doesn't work either. Am absolutely stuck and need help. I have a feeling it has to do with nested observables, and maybe tagging #observable isn't good enough, but not quite sure. repo and canvas are marked as observables and instantiate a new Repo() object and new Canvas() object, much like new Node() is created in getNodes.
Mobx (v4) does not track addition or removal of entries in an observable object unless observable.map is used. Or upgrading to mobx v5 should solve the issue.
For your specific issue you can try:
#observable nodeIdToNodeData = {};
//...
this.nodeIdToNodeData = {...this.nodeIdToNodeData, [currNode.id]: currNode};
Or try to upgrade to mobx v5.
More info here
Looks like Edward solved it, similar to Redux it looks like you have to create a new object with the dictionary rather than modify it. I'm guessing it's because the key currNode.id is already defined (as the value undefined), and we're just modifying it to be currNode. That's my guess, regardless it works now.
I see that the ag-grid-react repo has types, and I also see that the ag-grid-react-example repo has examples. But how do I put the two together and create a cell editor with React and Types?
I'm guessing it's something like this but I can't make TypeScript happy:
class MyCellEditor implements ICellEditorReactComp {
public getValue() {
// return something
}
public render() {
const { value } = this.props
// return something rendering value
}
}
I implemented ICellEditor and used ICellEditorParams for prop definitions. For example, this MyCellEditor example from their documentation:
// function to act as a class
function MyCellEditor () {}
// gets called once before the renderer is used
MyCellEditor.prototype.init = function(params) {
// create the cell
this.eInput = document.createElement('input');
this.eInput.value = params.value;
};
// gets called once when grid ready to insert the element
MyCellEditor.prototype.getGui = function() {
return this.eInput;
};
// focus and select can be done after the gui is attached
MyCellEditor.prototype.afterGuiAttached = function() {
this.eInput.focus();
this.eInput.select();
};
// returns the new value after editing
MyCellEditor.prototype.getValue = function() {
return this.eInput.value;
};
// any cleanup we need to be done here
MyCellEditor.prototype.destroy = function() {
// but this example is simple, no cleanup, we could
// even leave this method out as it's optional
};
// if true, then this editor will appear in a popup
MyCellEditor.prototype.isPopup = function() {
// and we could leave this method out also, false is the default
return false;
};
became this:
class MyCellEditor extends Component<ICellEditorParams,MyCellEditorState> implements ICellEditor {
constructor(props: ICellEditorParams) {
super(props);
this.state = {
value: this.props.eGridCell.innerText
};
}
// returns the new value after editing
getValue() {
// Ag-Grid will display this array as a string, with elements separated by commas, by default
return this.state.value;
};
// Not sure how to do afterGuiAttached()
// if true, then this editor will appear in a popup
isPopup() {
return true;
};
render() {
return (
<div>
hello
</div>
);
}
}
I am new to react.
I want to add another SingleButtonComponent when a SingleButtonComponent is clicked.
I am able to increment the number of children using a state variable numChildren.
AddChild: function() {
var numChildren = (this.state.numChildren) +1;
this.setState({numChildren :numChildren})
},
But I am getting error whenever I am trying to loop inside
render: function () {
return (
for (var i = 0; i < this.state.numChildren; i += 1) {
<SingleButtonComponent AddChild={this.AddChild}/>
};
)
}
. Here is the PLUNKER to it
I have temporarily kept the for loop out of render..
Please suggest me a way to add a child-component every time it is clicked.
Thanks
Your render function doesn't return anything.
If you're using React 16, try:
render: function() {
const arr = [];
for (var i = 0; i < this.state.numChildren; i += 1) {
arr.push(<SingleButtonComponent key={i} AddChild={this.AddChild}/>);
}
return arr;
}
Otherwise, you must return a single element:
render: function() {
const arr = [];
for (var i = 0; i < this.state.numChildren; i += 1) {
arr.push(<SingleButtonComponent key={i} AddChild={this.AddChild}/>);
}
return (<div>{arr}</div>);
}
Regarding your second question:
React.createClass is deprecated in favor of ES6 classes. Your component should be defined as follows:
class MainBody extends React.Component {
constructor() {
this.state = { numChildren: 0 };
}
AddChild() { ... }
render() { ... }
}
Trying to essentially accomplish this https://github.com/elgerlambert/redux-localstorage which is for Redux but do it for Mobx. And preferably would like to use sessionStorage. Is there an easy way to accomplish this with minimal boilerplate?
The easiest way to approach this would be to have a mobx "autorun" triggered whenever any observable property changes. To do that, you could follow my answer to this question.
I'll put some sample code here that should help you get started:
function autoSave(store, save) {
let firstRun = true;
mobx.autorun(() => {
// This code will run every time any observable property
// on the store is updated.
const json = JSON.stringify(mobx.toJS(store));
if (!firstRun) {
save(json);
}
firstRun = false;
});
}
class MyStore {
#mobx.observable prop1 = 999;
#mobx.observable prop2 = [100, 200];
constructor() {
this.load();
autoSave(this, this.save.bind(this));
}
load() {
if (/* there is data in sessionStorage */) {
const data = /* somehow get the data from sessionStorage or anywhere else */;
mobx.extendObservable(this, data);
}
}
save(json) {
// Now you can do whatever you want with `json`.
// e.g. save it to session storage.
alert(json);
}
}
Turns out you can do this in just a few lines of code:
const store = observable({
players: [
"Player 1",
"Player 2",
],
// ...
})
reaction(() => JSON.stringify(store), json => {
localStorage.setItem('store',json);
}, {
delay: 500,
});
let json = localStorage.getItem('store');
if(json) {
Object.assign(store, JSON.parse(json));
}
Boom. No state lost when I refresh the page. Saves every 500ms if there was a change.
Posting the example from here: https://mobx.js.org/best/store.html
This shows a cleaner method of detecting value changes, though not necessarily local storage.
import {observable, autorun} from 'mobx';
import uuid from 'node-uuid';
export class TodoStore {
authorStore;
transportLayer;
#observable todos = [];
#observable isLoading = true;
constructor(transportLayer, authorStore) {
this.authorStore = authorStore; // Store that can resolve authors for us
this.transportLayer = transportLayer; // Thing that can make server requests for us
this.transportLayer.onReceiveTodoUpdate(updatedTodo => this.updateTodoFromServer(updatedTodo));
this.loadTodos();
}
/**
* Fetches all todo's from the server
*/
loadTodos() {
this.isLoading = true;
this.transportLayer.fetchTodos().then(fetchedTodos => {
fetchedTodos.forEach(json => this.updateTodoFromServer(json));
this.isLoading = false;
});
}
/**
* Update a todo with information from the server. Guarantees a todo
* only exists once. Might either construct a new todo, update an existing one,
* or remove an todo if it has been deleted on the server.
*/
updateTodoFromServer(json) {
var todo = this.todos.find(todo => todo.id === json.id);
if (!todo) {
todo = new Todo(this, json.id);
this.todos.push(todo);
}
if (json.isDeleted) {
this.removeTodo(todo);
} else {
todo.updateFromJson(json);
}
}
/**
* Creates a fresh todo on the client and server
*/
createTodo() {
var todo = new Todo(this);
this.todos.push(todo);
return todo;
}
/**
* A todo was somehow deleted, clean it from the client memory
*/
removeTodo(todo) {
this.todos.splice(this.todos.indexOf(todo), 1);
todo.dispose();
}
}
export class Todo {
/**
* unique id of this todo, immutable.
*/
id = null;
#observable completed = false;
#observable task = "";
/**
* reference to an Author object (from the authorStore)
*/
#observable author = null;
store = null;
/**
* Indicates whether changes in this object
* should be submitted to the server
*/
autoSave = true;
/**
* Disposer for the side effect that automatically
* stores this Todo, see #dispose.
*/
saveHandler = null;
constructor(store, id=uuid.v4()) {
this.store = store;
this.id = id;
this.saveHandler = reaction(
// observe everything that is used in the JSON:
() => this.asJson,
// if autoSave is on, send json to server
(json) => {
if (this.autoSave) {
this.store.transportLayer.saveTodo(json);
}
}
);
}
/**
* Remove this todo from the client and server
*/
delete() {
this.store.transportLayer.deleteTodo(this.id);
this.store.removeTodo(this);
}
#computed get asJson() {
return {
id: this.id,
completed: this.completed,
task: this.task,
authorId: this.author ? this.author.id : null
};
}
/**
* Update this todo with information from the server
*/
updateFromJson(json) {
// make sure our changes aren't send back to the server
this.autoSave = false;
this.completed = json.completed;
this.task = json.task;
this.author = this.store.authorStore.resolveAuthor(json.authorId);
this.autoSave = true;
}
dispose() {
// clean up the observer
this.saveHandler();
}
}
Here, you can use my code, although it only supports localStorage you should be able to modify it quite easily.
https://github.com/nightwolfz/mobx-storage