I am trying to use ace to create an sql editor, but I need a unique highlight. So, I need to create a custom mode that inherits from the existing sql mode.
The issue is, because ace uses window, and nextjs is ssr, I am unable to create a custom mode using ace's tutorials as I get the window is not defined error in the .ts file.
I can get around this error in the .tsx file by importing it with next/dynamic and disabling ssr for that component, but for the mode I am stumped.
SqlMode.js (it's not .ts because ace typing just doesn't work for me)
import ace from 'ace-builds/src-noconflict/ace';
import 'ace-builds/src-noconflict/mode-sql'
export class SqlHighlightRules extends window.ace.acequire('ace/mode/text_highlight_rules').TextHighlightRules {
constructor() {
super();
this.$rules.start.unshift({
token: 'text-orange-main',
regex: '{{*.}}',
})
}
}
export class SqlMode extends window.ace.acequire('ace/mode/sql').Mode {
constructor() {
super()
this.HighlightRules = SqlHighlightRules;
}
}
The compilation fails in this file, as it uses window, which is undefined.
Editor.tsx
import { useEffect, useRef, useState } from 'react';
import AceEditor from 'react-ace';
import 'ace-builds/src-noconflict/ace';
import 'ace-builds/src-noconflict/theme-tomorrow';
import { SqlMode } from './config/SqlMode';
import 'ace-builds/src-noconflict/ext-language_tools';
const styles = { borderRadius: 8 };
export default function Editor() {
const editor = useRef<AceEditor>();
const [code, setCode] = useState<string>('');
useEffect(() => {
const mode = new SqlMode();
//#ts-ignore
editor.current.editor.getSession().setMode(mode);
}, []);
return (
<AceEditor
ref={editor}
theme="tomorrow"
value={code}
onChange={setCode}
enableBasicAutocompletion
enableLiveAutocompletion
style={styles}
/>
);
}
This file works if used through next/dynamic with { ssr: false } and without the custom mode. But as soon as I use the custom mode, errors.
sql.page.tsx
import dynamic from 'next/dynamic';
const Editor = dynamic(() => import('../components/editor/Editor'), { ssr: false });
export default function SqlEditor() {
return (
<div className="w-full h-full flex justify-center items-center">
<Editor />
</div>
);
}
I would like to be able to create a custom highlight for the sql mode, while working with nextjs. If there is a solution I am not aware of, I'd be happy to learn.
Alternatively if there is another FE editor I could use that does largely the same thing as ace, that could also be useful.
Thank you.
I made a function component where I have registered quill-blot-formatter with react-quill and added the blotFormatter in the modules list. Then I imported this module with next/dynamic on the page I want.
The custom function:
import ReactQuill, { Quill } from 'react-quill';
import { BlotFormatter } from 'quill-blot-formatter';
import 'react-quill/dist/quill.snow.css'
Quill.register("modules/blotFormatter", BlotFormatter);
const modules = {
blotFormatter: {
overlay: {
style: { border: '2px solid red',}
}},
toolbar: [...],
}
const formats = [...];
I am calling the default ReactQuill export from react-quill like this:
export default function QuillCustom({onChange}) {
return (
<ReactQuill
modules={modules}
formats={formats}
theme="snow"
onChange={onchange}
readOnly={false}
/>
);
}
And on a Nextjs page component I'm calling it like this:
const QuillNoSSRWrapper = dynamic(() => import('../Components/quillComponent'), {
ssr: false,
loading: () => <p>Loading...</p>,
})
return (
<div>
<QuillNoSSRWrapper
className={styles.quillTextArea}
onChange={handleTextChange}
/>
</div>
)
Now, the issue is that, after the page loads, the loading... prop of declared in const QuillNoSSRWrapper shows on the screen and stays there forever. The quill editor doesn't appear.
I tried commenting this line: Quill.register("modules/blotFormatter", BlotFormatter); on the custom module and then the quill editor appeared. Is the blotFormatter module not registering with Quill? How do I register that then?
Sorry, my bad!
BlotFormatter is the default import.
import BlotFormatter from 'quill-blot-formatter';
I wanted to know how can I add a class to a modal in a navbar components? My navbar is in App.vue and I wanted to create a message that would add the class "is-active" to a modal in my navbar when I click on it. But I can't find the way to do that..
Thank you
Usually when you have a parent -> child relationship you can use events. In this case since you have two components that are not linked (directly) then you have two alternatives.
Using store (it is usually used in cases where your application is of a considerate size)
You can use vuex to have a central place where you will have your global state. A simple example would be:
store/main.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
isModalOpen: false
},
getters: {
isModalOpen => (state) => state.isModalOpen,
},
mutations: {
setIsModalOpen (state, isOpen) {
state.isModalOpen = isOpen;
}
}
})
then you can access the store in your component as such:
<template>
<navbar :class="[isNavBarOpen ? "is-active" : ""]" />
</template>
export default {
computed: {
isNavBarOpen () {
this.$store.getters['isModalOpen']
}
}
}
Event bus (it is usually used in cases where you have a small app and do not need a global state manager)
Read more about EventBus here.
You can create a simple EventBus
services/eventBus.js
import Vue from 'vue';
const export EventBus = new Vue();
then on your component when the modal is open you can do:
// # -> is an alias to your root folder. Most projects scafolded by Vue CLI has this by default
import {EventBus} from "#/services/eventBus"
export default {
methods: {
openStore: () => {
// your logic to open modal
EventBus.$emit('modal-open');
}
}
}
then on your App.vue you just listen to this event
App.vue
<template>
<navbar :class="[isModalOpen ? "is-active" : ""]" />
</template>
// # -> is an alias to your root folder. Most projects scafolded by Vue CLI has this by default
import {EventBus} from "#/services/eventBus"
export default {
data() {
return {
isModalOpen: false,
}
},
created() {
EventBus.$on('modal-open', this.onModalOpen);
},
methods: {
onModalOpen() {
this.isModalOpen = true;
}
}
}
The one you will pick depends on our application structure and if you think it is complex enough to use a central state management (vuex).
There might contain some errors in the code but the main idea is there.
I'm building a React application and I started using CRA. I configured the routes of the app using React Router. Pages components are lazy-loaded.
There are 2 pages: Home and About.
...
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));
...
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path="/about" component={About} />
<Route path="/" component={Home} />
</Switch>
</Suspense>
...
Each page uses the Button component below.
import React from 'react';
import styles from './Button.module.scss';
const Button = ({ children, className = '' }) => (
<button className={`${styles.btn} ${className}`}>{children}</button>
);
export default Button;
The Button.module.scss file just sets the background color of the button to red.
.btn {
background: red;
}
The Button component accepts a className prop which is then added to the rendered button. This is because I want to give freedom to the consumer of the component. For example, in some pages margins could be needed or the background should be yellow instead of red.
To make it simple, I just want to have a different background color for the Button based on the current page, so that:
Home page => Blue button
About page => Yellow button
Each page is defined as below:
import React from 'react';
import Button from './Button';
import styles from './[PageName].module.scss';
const [PageName] = () => (
<div>
<h1>[PageName]</h1>
<Button className={styles.pageBtn}>[ExpectedColor]</Button>
</div>
);
export default [PageName];
where [PageName] is the name of the page and [ExpectedColor] is the corresponding expected color based on the above bullet list (blue or yellow).
The imported SCSS module, exports a class .pageBtn which sets the background property to the desired color.
Note: I could use a prop on the Button component which defines the variant to display (Blue/Yellow) and based on that prop add a class defined in the SCSS file. I don't want to do that since the change could be something that doesn't belong to a variant (e.g. margin-top).
The problem
If I run the application using yarn start, the application works fine. However, if I build the application (yarn build) and then I start serving the application (e.g. using serve -s build), the behavior is different and the application doesn't work as expected.
When the Home page is loaded, the button is correctly shown with a blue background. Inspecting the loaded CSS chunk, it contains:
.Button_btn__2cUFR {
background: red
}
.Home_pageBtn__nnyWK {
background: blue
}
That's fine. Then I click on the navigation link to open the About page. Even in this case, the button is shown correctly with a yellow background. Inspecting the loaded CSS chunk, it contains:
.Button_btn__2cUFR {
background: red
}
.About_pageBtn__3jjV7 {
background: yellow
}
When I go back to the Home page, the button is now displayed with a red background instead of yellow. That's because the About page has loaded the CSS above which defines again the Button_btn__2cUFR class. Since the class is now after the Home_pageBtn__nnyWK class definition, the button is displayed as red.
Note: the Button component is not exported on the common chunk because its size is too small. Having that in a common chunk could solve the problem. However, my question is about small shared components.
Solutions
I have thought to 2 solutions which, however, I don't like too much:
Increase selectors specificity
The classes specified in the [PageName].module.scss could be defined as:
.pageBtn.pageBtn {
background: [color];
}
This will increase the selector specificity and will override the default Button_btn__2cUFR class. However, each page chunk will include the shared components in case the component is quite small (less than 30kb). Also, the consumer of the component has to know that trick.
Eject and configure webpack
Ejecting the app (or using something like react-app-rewired) would allow specifying the minimum size for common chunk using webpack. However, that's not what I would like for all the components.
To summarize, the question is: what is the correct working way of overriding styles of shared components when using lazy-loaded routes?
You can use the following logic with config file for any pages. Also, You can send config data from remote server (req/res API) and handle with redux.
See Demo: CodeSandBox
create components directory and create files like below:
src
|---components
|---Button
| |---Button.jsx
| |---Button.module.css
Button Component:
// Button.jsx
import React from "react";
import styles from "./Button.module.css";
const Button = props => {
const { children, className, ...otherProps } = props;
return (
<button className={styles[`${className}`]} {...otherProps}>
{children}
</button>
);
};
export default Button;
...
// Button.module.css
.Home_btn {
background: red;
}
.About_btn {
background: blue;
}
create utils directory and create AppUtils.js file:
This file handle config files of pages and return new object
class AppUtils {
static setRoutes(config) {
let routes = [...config.routes];
if (config.settings) {
routes = routes.map(route => {
return {
...route,
settings: { ...config.settings, ...route.settings }
};
});
}
return [...routes];
}
static generateRoutesFromConfigs(configs) {
let allRoutes = [];
configs.forEach(config => {
allRoutes = [...allRoutes, ...this.setRoutes(config)];
});
return allRoutes;
}
}
export default AppUtils;
create app-configs directory and create routesConfig.jsx file:
This file lists and organizes routes.
import React from "react";
import AppUtils from "../utils/AppUtils";
import { pagesConfig } from "../pages/pagesConfig";
const routeConfigs = [...pagesConfig];
const routes = [
...AppUtils.generateRoutesFromConfigs(routeConfigs),
{
component: () => <h1>404 page not found</h1>
}
];
export default routes;
Modify index.js and App.js files to:
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
rootElement
);
...
react-router-config: Static route configuration helpers for React
Router.
// App.js
import React, { Suspense } from "react";
import { Switch, Link } from "react-router-dom";
import { renderRoutes } from "react-router-config";
import routes from "./app-configs/routesConfig";
import "./styles.css";
export default function App() {
return (
<div className="App">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ul>
<Suspense fallback={<h1>loading....</h1>}>
<Switch>{renderRoutes(routes)}</Switch>
</Suspense>
</div>
);
}
create pages directory and create files and subdirectory like below:
src
|---pages
|---about
| |---AboutPage.jsx
| |---AboutPageConfig.jsx
|
|---home
|---HomePage.jsx
|---HomePageConfig.jsx
|
|---pagesConfig.js
About Page files:
// AboutPage.jsx
import React from "react";
import Button from "../../components/Button/Button";
const AboutPage = props => {
const btnClass = props.route.settings.layout.config.buttonClass;
return (
<>
<h1>about page</h1>
<Button className={btnClass}>about button</Button>
</>
);
};
export default AboutPage;
...
// AboutPageConfig.jsx
import React from "react";
export const AboutPageConfig = {
settings: {
layout: {
config: {
buttonClass: "About_btn"
}
}
},
routes: [
{
path: "/about",
exact: true,
component: React.lazy(() => import("./AboutPage"))
}
]
};
Home Page files:
// HomePage.jsx
import React from "react";
import Button from "../../components/Button/Button";
const HomePage = props => {
const btnClass = props.route.settings.layout.config.buttonClass;
return (
<>
<h1>home page</h1>
<Button className={btnClass}>home button</Button>
</>
);
};
export default HomePage;
...
// HomePageConfig.jsx
import React from "react";
export const HomePageConfig = {
settings: {
layout: {
config: {
buttonClass: "Home_btn"
}
}
},
routes: [
{
path: "/",
exact: true,
component: React.lazy(() => import("./HomePage"))
}
]
};
...
// pagesConfig.js
import { HomePageConfig } from "./home/HomePageConfig";
import { AboutPageConfig } from "./about/AboutPageConfig";
export const pagesConfig = [HomePageConfig, AboutPageConfig];
Edited section:
With HOC Maybe this way: CodeSandBox
create hoc dir and withPage.jsx file:
src
|---hoc
|---withPage.jsx
...
// withPage.jsx
import React, { useEffect, useState } from "react";
export function withPage(Component, path) {
function loadComponentFromPath(path, setStyles) {
import(path).then(component => setStyles(component.default));
}
return function(props) {
const [styles, setStyles] = useState();
useEffect(() => {
loadComponentFromPath(`../pages/${path}`, setStyles);
}, []);
return <Component {...props} styles={styles} />;
};
}
And then pages like below:
src
|---pages
|---about
| |---About.jsx
| |---About.module.css
|
|---home
|---Home.jsx
|---Home.module.css
About.jsx file:
// About.jsx
import React from "react";
import { withPage } from "../../hoc/withPage";
const About = props => {
const {styles} = props;
return (
<button className={styles && styles.AboutBtn}>About</button>
);
};
export default withPage(About, "about/About.module.css");
About.module.css file:
// About.module.css
.AboutBtn {
background: yellow;
}
Home.jsx file:
// Home.jsx
import React from "react";
import { withPage } from "../../hoc/withPage";
const Home = props => {
const { styles } = props;
return <button className={styles && styles.HomeBtn}>Home</button>;
};
export default withPage(Home, "home/Home.module.css");
Home.module.css file:
// Home.module.css
.HomeBtn {
background: red;
}
I would suggest instead of adding both the default styles and the consumer styles, use the consumer's styles over yours and use your as a callback if not supplied. The consumer can still compose your defaults with the composes keyword.
Button.js
import React from 'react';
import styles from './Button.module.scss';
const Button = ({ children, className}) => (
<button className={className ?? styles.btn}>{children}</button>
);
export default Button;
SomePage.module.scss
.pageBtn {
// First some defaults
composes: btn from './Button.module.scss';
// And override some of the defautls here
background: yellow;
}
If you wish, use sass #extends or #mixin instead
EDIT: Haven't tested it, but could it be that just by using composes webpack will make sure to bundle the defaults only once? Thus you're no longer needed to change your Button.js code with the ??
Solution 1
I know this is very obvious, but would work anyway:
Set !important on your overwriting css rules, thus bypassing specificity:
[PageName].module.scss:
.btn {
color: yellow !important;
}
However, most of the strict devs I know would avoid this keyword at all cost.
Why ?
Because when you start to have a lot of !important your css is a nightmare to debug. If you start writing !important rules with higher specificity, you know you have gone too far
It is only meant for corner-cases like yours, you might as well use it.
Solution 2
fix CRA config to enforce style tags order.
It is open-source after all :)
You can give your input on this bug here (upvote might give it more visibility):
https://github.com/facebook/create-react-app/issues/7190
Solution 3 (Update)
You could create a SCSS mixin in a new customButton.scss file, to generate css rules with higher specificity:
// customButton.scss
#mixin customBtn() {
:global {
.customBtn.override {
#content;
}
}
}
We will use two static class names (using the :global selector), because that way their name won't change based on where they are imported from.
Now use that mixin in your pages' SCSS:
// [pageName].module.scss
#import 'customButton.scss';
#include customBtn {
color: yellow;
}
css output should be:
.customBtn.override {
// put everything you want to customize here
color: yellow;
}
In Button.jsx: apply both class names to your button in addition to styles.btn:
// Button.jsx
const Button = ({ children, className = '' }) => (
<button className={`${styles.btn} customBtn override ${className}`}>
{children}
</button>
);
(Note that these are not referenced through the styles object, but the classname directly)
The main drawback is these are not dynamic class names, so you have to watch out to avoid conflicts yourself like we use to do before.
But I think it should do the trick
I'm using the react toolbox in my react project. Is there a way to set some default styles which override the styles from react-toolbox?
Wan't to change some colors and paddings?
Thanks for your help.
Can I do this with something like this:
postcssPlugins: [
cssnext({
features: {
customProperties: {
variables: {
'color-primary': 'var(--palette-amber-500)',
},
},
},
}),
solution is to create a theme.js component which overwrites the react-toolbox stuff
import RTInput from './input.css';
import RTTooltip from './tooltip.css'
export default {
RTInput, RTTooltip
};
And then You need a theme-provider .. I did this in index.js
import {ThemeProvider} from 'react-css-themr';
import theme from './styles/react-toolbox-customs/theme.js';
<ThemeProvider theme={theme}>
And wrap your content
thats it.