Fabric js edit text added in canvas via input - javascript

I am using fabricjs and fabricjs-react in react app. I need the text to be edited via input. But it doesn't work. Maybe I'm missing something?
But when I enter text into the input, the canvas is not updated and the text remains the same. What am I doing wrong? How to access existing text in fabricjs?
There is my code https://codesandbox.io/s/sleepy-stitch-yvcr22
UPD: I tried to do https://codesandbox.io/s/clever-christian-zxb5ge with the addition of ID. It works. Maybe there is some more universal solution?
import React, { useEffect, useState } from 'react'
import { FabricJSCanvas, useFabricJSEditor } from 'fabricjs-react'
import { fabric } from 'fabric'
export default function App() {
const { editor, onReady } = useFabricJSEditor()
const [text, setText] = useState('')
let headText = new fabric.Text("test", {
fill: 'black',
top: 50,
left: 50,
})
const _onReady = (canvas) => {
canvas.backgroundColor = 'yellow'
canvas.setDimensions({
width: 200,
height: 200,
})
canvas.add(headText);
canvas.renderAll()
onReady(canvas)
}
useEffect(() => {
if (editor) {
headText.text = text
editor.canvas.renderAll()
}
}, [text])
return (
<>
<input onChange={(e) => setText(e.target.value)} />
<FabricJSCanvas onReady={_onReady} />
</>
)
}

The reason why it doesn't work is because whenever the setText() state is called, the App() function is called again in order to render the state changes.
Here lies the problem with this; any subsequence call to App() you end up creating a new instance of headText
let headText = new fabric.Text("test", {
fill: 'black',
top: 50,
left: 50,
})
Your useEffect is using that new instance whereas the canvas contains the very first instance. So, any changes you attempt is referring to the wrong object.
This quick dirty check proves this is a different object:
useEffect(() => {
if (editor) {
editor.canvas.getObjects()[0].text = text;
editor.canvas.renderAll()
console.log(editor.canvas.getObjects()[0] === headText);
}
}, [text])
The solution is that you need to give headText its own state so that any subsequence calls to App() the object reference is preserved.
const [headText, setTextObject] = useState(
new fabric.Text('test', {
fill: 'black',
top: 50,
left: 50,
}));

Related

How to use string to find object key?

How can I select an object key in a theme object using a string used in react component prop?
theme
{
palette: {
primary: {
main: '#dc2626',
dark: '#7f1d1d',
light: '#fecaca',
}
text: '#111827',
}
}
component
const Typography = ({ color, children }) => (
<p style={{ color: theme.palette[color] }}>{children}</p>
);
How I want to use the component:
<Typography color='primary.main'>Hello world</Typography>
Let me know if this is a bad practice and should be avoided anyway.
I tried to use the eval function but found out it was not secure and considered to be a bad practice.
If you are just looking how to get a nested field in an object, you should look at lodash's get function.
Example :
export default const theme = {
palette: {
primary: {
main: '#dc2626',
dark: '#7f1d1d',
light: '#fecaca',
}
text: '#111827',
}
};
Component
import { get } from lodash library;
import theme from './theme.js';
export default function MyComponent({ children }) {
const nestedFieldPath = 'palette.primary.main';
// Get nested field value
const nestedFieldValue = get(theme, nestedFieldPath);
return <p color={nestedFieldValue}>{children}</p>
}
You could make a hook that can be extended as well as handle a default incase no color is being passed to your typography component
Example hook:
import React from "react";
const useColor = (color = null) => {
// Set the default theme
const [selectedTheme, setSelectedTheme] = React.useState(["primary", "main"]);
React.useEffect(() => {
if (color) {
setSelectedTheme(color.split("."));
}
}, [color]);
return selectedTheme;
};
export default useColor;
Then the Typography component you're returning an array from the hook so deconstruct it into 2 values. Naming is of course questionable here :
const Typography = ({ color, children }) => {
const [level, choice] = useColor(color);
return <p style={{ color: theme.palette[level][choice] }}>{children}</p>;
};
Usage:
<Typography color={"primary.main"}>Hello</Typography>
A codesandbox : https://codesandbox.io/s/clever-julien-rr0tuf?file=/src/App.js:506-561
This would be my solution, without using any additional libraries.
Idea is to create a function with method for getting the nested color value from your Theme object and then use that function (getColor) in your components.
This can handle additional levels of nesting.
Bare in mind, my experience with React is reading 3 pages of docs.
It is not clear from question how you want to handle situation when you try to get color that is not in theme, you would have to handle that inside reduce.
function Theme () {
let palette = {
primary: {
main: '#dc2626',
dark: '#7f1d1d',
light: '#fecaca',
},
secondary : {
green: {
lightgreen : 'myfancycolor :)'
}
}
}
this.getColor = function(string){
let nesting = string.split(".");
const result = nesting.reduce(
(accumulator, currentValue) => {
if (accumulator[currentValue])
return accumulator[currentValue];
else
return accumulator
} ,
palette
);
return result;
}
}
let myTheme = new Theme();
console.log(myTheme.getColor("secondary.green.lightgreen"))
//prints myfancycolor :)
Usage in component:
const Typography = ({ color, children }) => (
<p style={{ color: myTheme.getColor("primary.dark") }}>{children}</p>
);

How to override CSS classes made with makeStyles

Note, I'm using MUI 4.12.3. In file A I have this (simplified) besides a functional component. Inside the functional component's return statement I also apply it to a JSX element (not shown). This works well.
const useStyles = makeStyles((t) => ({
content: {
minHeight: '100vh',
},
}));
In file B, I would like to override the CSS class content based on isDesktop. Is this possible/desirable? Something like this, but it does not work:
const useStyles = makeStyles({
content: {
minHeight: (props) => (props.isDesktop ? '100vh' : '112vh'),
},
});
//And inside my functional component:
const isDesktop = useMediaQuery(Theme.breakpoints.up('sm'));
const classes = useStyles({ isDesktop });
Note that the intent is to not render the JSX component in file B, only to override the CSS class content in file B is desirable. (classes is unused in my sample.)
This can be done with few hooks.Let say my functional componet name is "Mycomponent".my material component is "materialcomponent".we need to import useWindowSize hook.That helps us to get the window size.so that we can check our screen size is desktop or mobile.in the makestyle we have to create two classes for desktop and mobile minHeight.with the simple if else checking we can pass this two classes conditionally to materialcomponent className prop.Below is the code.
1.create two classes with the makestyles
const useStyles = makeStyles((t) => ({
contentDesktop: {
minHeight: '100vh',
},
contentMobile:{
minHeight: '110vh',
}
}));
2.import the useWindowSize hook
import useWindowSize from "hooks/useWindowSize";
3.functinal componet code.
const Mycomponent = () => {
const classes = useStyles();
let myclass="";
const width = useWindowSize();
const isDesktop = width >=1024;
const mobile= width <= 600;
if(isDesktop){
myclass=classes. contentDesktop;
}
if(mobile){
myclass=classes.contentMobile;
}
return (
<materialcomponent className={`${myclass}`}
);
}
You can export this function outside of your functional component
export const getStyles = (isDesktop) => {
return {
content: {
minHeight: isDesktop ? "100vh" : "112vh",
},
};
};
And then wherever you want your styling applied
const isDesktop = useMediaQuery(Theme.breakpoints.up('sm'));
...
<SomeMuiComponent sx={getStyles(isDekstop)} />

React.js Component children to DOM element

I am trying to create my own Map component and one of the tasks is to create custom Map Annotation. This is how it looks like a MapAnnotation component in the render function
<MapAnnotation title='Annotation' latitude={ 12.345678 } longitude={ 12.345678 }>
<div style={{width: '100px', height: '100px', backgroundColor: 'lightblue'}} />
</MapAnnotation>
The method that converts MapAnnotation component into the map object is pretty simple:
createAnnotation(child) {
const { latitude, longitude, title } = child.props
var coordinate = new global.mapkit.Coordinate(latitude, longitude)
if ('children' in child.props) {
var newAnnotation = new global.mapkit.Annotation(coordinate, function(coordinate, options) {
var domElement =
return ReactDOMServer.renderToString(<div>{child.props.children}</div>)
}, { title: title })
return newAnnotation
} else {
var newAnnotation = new global.mapkit.MarkerAnnotation(coordinate, { title: title })
return newAnnotation
}
}
Unfortunately, I do not know how to return the DOM element from children that exist in MapAnnotation. Right now I am getting the error:
Error: [MapKit] Annotation element factory must return a DOM element,
got <div data-reactroot=""><div style="width:100px;height:100px;background-color:lightblue"></div></div>
instead.
I am the React.js beginner and most of my knowledge I have from ReactNative. This is the reason why I do not know what is the keyword to find the solution with Uncle Google.
I will be glad for help :)
The solution based on Boykov answer:
var domElement = document.createElement("div")
domElement.innerHTML = ReactDOMServer.renderToString(child.props.children)
return domElement
I think I can do it using useRef.
import react,{useRef, useEffect} from 'react';
const Test = ({book_id, chart_id, title, data_source}) => {
const refCom = useRef(null);
useEffect(() => {
if (refCom && refCom?.current) {
//Run the code you want to do inside.
console.log(refCom.current);
}
}, [refCom]);
return (<div ref={refCom}>....</div>)

NextJS: Images loaded from cache don't trigger the onLoad event

I am currently in the process of converting a ReactJS client side rendered application to a NextJS application for search engine optimization and social media linking reasons.
One of the components converted, which is basically an image that waits until it's finished loading then fades in, is not working as expected after it is used in a NextJS environment.
It behaves in the following manner:
Cache Enabled:
The first time the image loads and the onLoad event triggers thus showing the image.
The second time the image stays hidden because the onLoad event doesn't trigger when the image is loaded from cache.
Cache Disabled using devtools:
The onLoad event always works because the image is never served from cache.
Expected behavior and the behavior previously achieved with using just ReactJS:
The onLoad event should trigger whether the image was loaded from the cache or not.
When not using React this problem is usually caused when someone sets the images src before defining a onload function:
let img = new Image()
img.src = "img.jpg"
img.onload = () => console.log("Image loaded.")
Which should be:
let img = new Image()
img.onload = () => console.log("Image loaded.")
img.src = "img.jpg"
Here is simplified code that causes the same problem in NextJS:
import React, { useState } from "react"
const Home = () => {
const [loaded, setLoaded] = useState(false)
const homeStyles = {
width: "100%",
height: "96vh",
backgroundColor: "black"
}
const imgStyles = {
width: "100%",
height: "100%",
objectFit: "cover",
opacity: loaded ? 1 : 0
}
const handleLoad = () => {
console.log("Loaded")
setLoaded(true)
}
return (
<div className="Home" style={homeStyles}>
<img alt=""
onLoad={handleLoad}
src="https://images.unsplash.com/photo-1558981001-5864b3250a69?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80"
style={imgStyles}
/>
</div>
)
}
export default Home
I ended up using ImageObject.complete as a workaround thanks to someone's suggestion.
I used useRef to reference the image and checked if the image.current.complete === true on component mount.
Here is the code:
import React, { useEffect, useRef, useState } from "react"
const Home = () => {
const [loaded, setLoaded] = useState(false)
const image = useRef()
const homeStyles = {
width: "100%",
height: "96vh",
backgroundColor: "black"
}
const imgStyles = {
width: "100%",
height: "100%",
objectFit: "cover",
opacity: loaded ? 1 : 0
}
const handleLoad = () => setLoaded(true)
useEffect(() => {
if (image.current.complete) setLoaded(true)
}, [])
return (
<div className="Home" style={homeStyles}>
<img alt=""
ref={image}
onLoad={handleLoad}
src="https://images.unsplash.com/photo-1558981001-5864b3250a69?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1350&q=80"
style={imgStyles}
/>
</div>
)
}
export default Home
From Next.js v11.0.2-canary.4 onwards, one can directly use onLoadingComplete prop.
<Image src={src} onLoadingComplete={() => setLoaded(true)} />
How does this compare with other options??
You get to keep using the awesome next/image component instead of unoptimized img tag without any extra code or dealing with refs yourself.
If you are using the new Image component from nextjs 10, which does not support a forward ref, you can modify the workaround with
const [loaded, setLoaded] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if ((ref.current?.firstChild?.firstChild as HTMLImageElement | undefined)?.complete) {
setLoaded(true)
}
}, [])
return <div ref={ref}>
<Image src={src}
onLoad={() => setLoaded(true)}/>
</div>
I only want to add to #mohamed-seif-khalid answer that I had to add a check in useEffect if ref consist anything, because my render continued to break.
useEffect(() => {
if (image.current && image.current.complete) {
handleImageLoad()
}
}, []);

Trying to change background by updating state in Reactjs

So I am writing an app which has several games on it.
I want the user to be able to click a button and the game appears on the screen, and the background changes color.
I want to control this using state so that the background of the whole app changes, not just the game component.
With my code the way it is now, the state changes when the game is out, but the page does not update. Any advice?
this is how my state looks
state = {
madLibsOut: false,
ticTacOut: false,
pigLatinOut: false,
background: "green",
}
here is an example of my trying to change it. The state does change, and the game appears on the screen, but the background color does not update.
handleClickTicTacToe = () => {
const out = this.state.ticTacOut
const newColor = 'red'
this.setState({ ticTacOut: true, background: newColor })
}
just in case it is useful, here is how I am setting the style:
appStyle={
backgroundColor: this.state.background,
height: "100vh",
width: "100%",
}
render() {
return (
<div className="App" style={this.appStyle}>
Thanks!
Your issue is you're setting your state as a class variable and it gets instantiated once and the value will never change.
Assuming you have ES6 enabled. You should do something similar to what #Allessandro Messori said.
But in his solution it isn't good to modify the property of an object and set the state.
When setting the state you should usually create a new object to set the state with. In this case your style object should be a new object.
state = {
madLibsOut: false,
ticTacOut: false,
pigLatinOut: false,
appStyle: {
backgroundColor: "green",
height: "100vh",
width: "100%",
}
}
handleClickTicTacToe = () => {
const out = this.state.ticTacOut
// Use Object.assign(this.state.appStyle, { backgroundColor: 'red' }); if no ES6 enabled
const newStyle = { ...this.state.appStyle, backgroundColor: 'red' };
this.setState({ ticTacOut: true, appStyle: newStyle })
}
render() {
return (
<div className="App" style={this.state.appStyle}>
Another option is to create a function that returns your style which will always update based on your state. Assuming your original code you can update your code to this.
getAppStyle = () => {
return {
backgroundColor: this.state.background,
height: "100vh",
width: "100%",
};
}
render() {
const style = this.getAppStyle();
return (
<div className="App" style={style}>
Make appStyle a function that returns the style object based on your state.
appStyle = () => {
return {
backgroundColor:this.state.backgroundColor
}
}
You should out all the appStyle object inside your component state.
Something like this should work:
state = {
madLibsOut: false,
ticTacOut: false,
pigLatinOut: false,
appStyle:{
backgroundColor: "green",
height: "100vh",
width: "100%",
}
}
handleClickTicTacToe = () => {
const out = this.state.ticTacOut
let appStyle = this.state.appStyle
appStyle.backgroundColor = 'red'
this.setState({ticTacOut: true, appStyle:appStyle})
}
render() {
return (
<div className="App" style={this.state.appStyle}>

Categories

Resources