Project Structure
Overview
The project structure is organized to facilitate the development and maintenance of Qodly custom components. Below is an outline of the main directories and files in a typical project:
- node_modules: Contains project dependencies installed via npm.
- public: Contains static assets like images, fonts, or any files that should be publicly accessible.
- src: The main source code directory.
- components: The directory for custom components.
- ExampleComponent: An example custom component directory.
- ExampleComponent.build.tsx: Component build logic.
- ExampleComponent.render.tsx: Component rendering logic.
- ExampleComponent.settings.tsx: Component settings.
- ExampleComponent.config.tsx: Configuration for the component.
- index.tsx: Entry point for the component.
- index.tsx: The entry point for the components directory.
- ExampleComponent: An example custom component directory.
- App.tsx: Main application component.
- index.css: Global CSS styles.
- main.tsx: Entry point for the application.
- vite-env.d.ts: TypeScript declaration file for Vite.
- components: The directory for custom components.
- .eslintrc.cjs: ESLint configuration file.
- .gitignore: Specifies intentionally untracked files that Git should ignore.
- .prettierignore: Specifies files and directories to ignore for formatting using Prettier.
- .prettierrc: Configuration file for Prettier.
- index.html: Main HTML file for the application.
- package-lock.json: Lock file for npm dependencies.
- package.json: Project metadata and npm dependencies.
- proxy.config.ts: Configuration for the development server proxy.
- README.md: Project documentation.
- tsconfig.json: TypeScript configuration file.
- tsconfig.node.json: TypeScript configuration file for Node.js.
- vite.config.ts: Vite configuration file.
vite.config.ts
This file configures Vite for the project, incorporating React support and a custom redirection middleware. It defines the development server settings, including the host, port, and proxy configurations. The use of environment variables allows flexibility, and the redirection middleware enhances the development server's behavior.
Let's break down the contents of vite.config.ts:
1. Imports:
import { PluginOption, defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import proxy from './proxy.config';
The file begins by importing necessary modules. PluginOption and defineConfig are components from Vite, while react is a Vite plugin that adds support for React. Additionally, proxy is imported from proxy.config.ts to set up server proxy configurations.
2. Environment Variables:
const port = process.env.PORT || 5001;
const host = process.env.HOST || '0.0.0.0';
These lines check for the existence of environment variables PORT and HOST and assign default values (5001 and '0.0.0.0', respectively) if they are not provided.
3. Redirect Middleware:
const redirect = (opts: { from: string; to: string }): PluginOption => {
return {
name: 'redirect',
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url.startsWith(opts.from)) {
res.statusCode = 307;
res.setHeader('Location', opts.to);
res.setHeader('Content-Length', '0');
return res.end();
}
return next();
});
},
};
};
This section defines a function named redirect that returns a Vite plugin (PluginOption). The plugin is designed to configure the development server with a redirection middleware. If a request URL starts with opts.from, it responds with a redirect to opts.to.
4. Vite Configuration:
export default defineConfig({
plugins: [
react(),
redirect({
from: '/studio/',
to: '/',
}),
],
define: {
'process.env': {},
},
server: {
host,
proxy,
port: +port,
},
});
The main Vite configuration is defined using defineConfig. Here are the key parts:
- Plugins:
- The
reactplugin is included to enable React support. - The custom
redirectplugin is added to handle URL redirections (from/studio/to/).
- The
- Define:
- An empty object is defined under
define, which allows you to specify global constants accessible in your code.
- An empty object is defined under
- Server:
- The development server configurations are set, including the
host,proxysettings, andport.
- The development server configurations are set, including the
proxy.config.ts
This file serves the crucial role of configuring the development server proxy, enabling efficient routing of requests during the development phase.
It is an integral part of the project configuration and is imported and utilized within the vite.config.ts file.
Let's break down the contents of proxy.config.ts:
1. Imports:
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ProxyOptions } from 'vite';
The file starts by importing the ProxyOptions type from Vite. Additionally, it disables TypeScript explicit any rule (@typescript-eslint/no-explicit-any) since it deals with dynamic objects often encountered in proxy configurations.
2. Constants:
const TARGET = process.env.PROXY_SERVER || 'https://127.0.0.1:7443';
The TARGET constant is defined to store the target URL for the proxy. It defaults to https://127.0.0.1:7443 but can be overridden by the PROXY_SERVER environment variable.
3. Function:
function sanitizeSetCookie(cookie = '') {
return cookie.replace(/; secure/i, '');
}
The sanitizeSetCookie function is created to remove the "secure" attribute from cookies. It is used during the proxy response handling.
4. Proxy Configuration Options:
const proxyOpts = {
target: TARGET,
secure: false,
ws: true,
onProxyReq(proxyReq: any, req: any) {
// Request handling logic...
},
onProxyRes(proxyRes: any) {
// Response handling logic...
},
};
The proxyOpts object contains various configuration options for the development server proxy:
-
target: The target URL to which requests will be proxied.
-
secure: Boolean indicating whether the target server uses HTTPS.
-
ws: Boolean indicating whether to proxy WebSocket connections.
Two functions (onProxyReq and onProxyRes explained in points 4 & 5 below) are provided as options for handling request and response events during proxying.
5. Request Handling Logic:
onProxyReq(proxyReq: any, req: any) {
let body = '';
req.setEncoding('utf-8');
req.on('data', (data: string) => (body += data));
req.on('end', () => {
req.body = body;
});
proxyReq.setHeader('Host', new URL(TARGET).host);
proxyReq.setHeader('Origin', TARGET);
proxyReq.setHeader('Referer', TARGET);
},
This code defines the logic executed when handling a request during the proxying process. Let's break it down step by step:
-
onProxyReqis an event triggered when a request is being sent to the target server through the proxy. It takes as parameters:-
proxyReqis the HTTP request object being sent to the target server. -
reqis the original HTTP request object received from the client.
-
-
The code reads the data from the original request (
req) and assembles it into thebody. It sets the encoding toutf-8for proper character handling and attaches the assembledbodytoreq.body. -
Additional headers are set on the
proxyReqobject to ensure the target server correctly interprets the request. Specifically, it sets the 'Host', 'Origin', and 'Referer' headers based on the target URL.
6. Response Handling Logic:
onProxyRes(proxyRes: any) {
if (proxyRes.headers['Set-Cookie']) {
if (Array.isArray(proxyRes.headers['Set-Cookie'])) {
proxyRes.headers['Set-Cookie'] = proxyRes.headers['Set-Cookie'].map(sanitizeSetCookie);
} else {
proxyRes.headers['Set-Cookie'] = sanitizeSetCookie(proxyRes.headers['Set-Cookie']);
}
}
},
This code defines the logic executed when handling a response during the proxying process:
-
onProxyResis an event triggered when a response is received from the target server through the proxy. It takesproxyResas a parameter, which is the HTTP response object received from the target server. -
The code checks if the response headers contain 'Set-Cookie'. If it is present:
-
It checks if 'Set-Cookie' is an array. If it is, it applies the
sanitizeSetCookiefunction to each element usingmap. -
If 'Set-Cookie' is not an array, it applies the
sanitizeSetCookiefunction directly.
-
-
The
sanitizeSetCookiefunction is responsible for removing the 'secure' flag from the 'Set-Cookie' header, ensuring it is compatible with non-secure connections.
This logic ensures that the 'Set-Cookie' header in the response is appropriately processed and modified before being sent back to the client, addressing security considerations related to cookie handling.
7. Proxy Configuration Object:
const proxy: Record<string, string | ProxyOptions> = [
'/rest',
'/$lib',
'/api',
'/login.html',
'/css',
'/img',
'/js',
'/LSP',
'/remoteDebugger',
'/dataexplorer',
'/$shared',
].reduce(
(prev, cur) => ({
...prev,
[cur]: proxyOpts,
}),
{
'/rest/$upload': {
target: TARGET,
secure: false,
},
},
);
A proxy configuration object is created by applying proxyOpts to specified paths. It utilizes the reduce function to accumulate the proxy configurations for various paths.
8. Export:
export default proxy;
The proxy object, containing the proxy configurations, is exported as the default export of the proxy.config.ts file.
index.html
This file serves as the main HTML file for the application, defining the structure and essential elements necessary for the web page. It acts as the entry point for the application when loaded in a web browser.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://use.fontawesome.com/107607b491.js"></script>
<script src="/$lib/dist/bundle.min.js"></script>
<title>qodly-example</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Within the body section, the following key components are present:
-
Root Div: A div element with the ID
rootis defined. This div typically serves as the mounting point for React components. -
Script for Application Entry: The main TypeScript module for the application (
main.tsx), serving as the entry point to initialize and execute the core logic of the application.
main.tsx
This file functions as the entry point for the application, initializing the React app by rendering the main application component. Let's break down the contents of main.tsx:
1. React Imports:
import React from 'react';
import ReactDOM from 'react-dom';
These imports bring in the necessary React and ReactDOM libraries for building and rendering React components.
2. Styling Import:
import './index.css';
This line imports the styles from index.css, providing styling for the components in the application.
3. Application Component Import:
import App from './App.tsx';
The main application component, App, is imported from the App.tsx file. This component serves as the root component for the entire application.
4. Rendering the App:
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')!,
);
The ReactDOM.render function is called to render the App component. The entire application is enclosed in React.StrictMode, which performs additional runtime checks for potential issues. The rendered content is then injected into the HTML element with the ID root. This root element is the div specified in the index.html file, where the React components will be mounted.
App.tsx
This file serves as the core application component, employing the WebformEditorStandalone component and integrating custom components from the components directory. This underscores that the core functionality and user interface of the application reside within the WebformEditorStandalone.
Let's break down the contents of App.tsx:
1. Imports:
import { WebformEditorStandalone } from '@ws-ui/webform-editor-standalone';
import components from './components';
-
The
WebformEditorStandalonecomponent is imported from the@ws-ui/webform-editor-standalonelibrary, serving as a specialized editor for web forms. -
The
componentsimport from the./componentsdirectory indicates the presence of custom components defined within that directory.
2. App Function Component:
function App() {
return <WebformEditorStandalone userComponents={components} />;
}
The App function component is defined, returning the WebformEditorStandalone component. This component is responsible for rendering and managing the web form editor, with userComponents as a prop set to the imported components.
3. Export Default:
export default App;
The App function component is the default export from the file, enabling other project files to effortlessly import and utilize it. In particular, it is used as the root component in main.tsx.
index.tsx - Component Index Entry
The index.tsx file in the components repository serves as the entry point for custom components, providing a centralized location for exporting and organizing multiple components. Let's delve into the details of this file:
1. Imports:
// imports
import ExampleComponent from './ExampleComponent';
The file begins with import statements, bringing in various components defined within the same directory. In this example, it specifically imports the ExampleComponent component.
2. Export Default Object:
export default {
// components
ExampleComponent,
};
The file exports a default object, functioning as a container for various components. This design facilitates easy access to components when the file is imported in the App.tsx file.
Custom Component repository
index.tsx
The index.tsx file serves as the epicenter of the ExampleComponent custom component, orchestrating its structure, behavior, and properties within the project. It dynamically renders either the Build or Render component based on the state of the enhanced editor, and it includes additional properties for crafting, information, and default props.
1. Imports:
import config, { IExampleComponentProps } from './ExampleComponent.config';
import { T4DComponent, useEnhancedEditor } from '@ws-ui/webform-editor';
import Build from './ExampleComponent.build';
import Render from './ExampleComponent.render';
-
config: Imports the configuration (ExampleComponent.config) and the props type (IExampleComponentProps) for theExampleComponent. -
T4DComponent: Imports a custom component type for the Webform Editor. -
useEnhancedEditor: Imports a hook from@ws-ui/Page-editorfor handling enhanced editor features. -
BuildandRender: Import components responsible for building and rendering theExampleComponent.
2. Component Declaration:
const ExampleComponent: T4DComponent<IExampleComponentProps> = (props) => {
-
Declares a constant variable
ExampleComponentof typeT4DComponent<IExampleComponentProps>, indicating that it is a custom component for the Page Editor. -
The component is a functional component that receives
propsof typeIExampleComponentProps.
3. Enhanced Editor State Check:
const { enabled } = useEnhancedEditor((state) => ({
enabled: state.options.enabled,
}));
-
Utilizes the
useEnhancedEditorhook to access the enhanced editor state. -
The
stateobject is destructured to obtain theenabledproperty, which represents whether the enhanced editor is enabled or not.
4. Conditional Rendering:
return enabled ? <Build {...props} /> : <Render {...props} />;
Conditionally renders either the Build or Render component based on the value of enabled:
-
If
enabledistrue, it renders theBuildcomponent, passing thepropsto it using the spread operator ({...props}). -
If
enabledisfalse, it renders theRendercomponent similarly.
5. Component Properties:
ExampleComponent.craft = config.craft;
ExampleComponent.info = config.info;
ExampleComponent.defaultProps = config.defaultProps;
Sets additional properties on the ExampleComponent. These properties are defined in the ExampleComponent.config.tsx file:
-
craft: Specifies the crafting configuration for the component. -
info: Provides information about the component. -
defaultProps: Sets default properties for the component.
6. Export Statement:
export default ExampleComponent;
Exports the ExampleComponent as the default export from the file, making it accessible for use in other parts of the project.
ExampleComponent.build.tsx
The ExampleComponent.build.tsx file defines the building logic for rendering the ExampleComponent within the context of the enhanced editor, utilizing the useEnhancedNode hook and applying provided styles and class names.
1. Imports:
import { useEnhancedNode } from '@ws-ui/webform-editor';
import cn from 'classnames';
import { FC } from 'react';
import { IExampleComponentProps } from './ExampleComponent.config';
-
useEnhancedNode: Imports a hook from@ws-ui/webform-editorfor handling enhanced node features. -
cn: Imports theclassnameslibrary, which is used for conditionally applying class names. -
FC: Imports theFC(Functional Component) type from React. -
IExampleComponentProps: Imports the props interface specific toExampleComponentfrom the configuration file.
2. Component Definition:
const ExampleComponent: FC<IExampleComponentProps> = ({ name, style, className, classNames = [] }) => {
-
Declares a constant variable
ExampleComponentas a functional component (FC) that takesIExampleComponentPropsas its props type. -
Destructures the props into individual variables (
name,style,className,classNames), providing default values where necessary.
3. Enhanced Node Connector:
const {
connectors: { connect },
} = useEnhancedNode();
Uses the useEnhancedNode hook to obtain a set of connectors, and by destructuring { connectors: { connect } }, it specifically extracts the connect connector. This connector is then utilized to establish a connection between the ExampleComponent and the enhanced features of the web form editor. It's a way for the component to integrate and interact with the editor's functionality.
4. Component Rendering:
return (
<span ref={connect} style={style} className={cn(className, classNames)}>
Hello {name}!
</span>
);
-
Returns JSX to render the component.
-
The
spanelement is connected to the enhanced node usingref={connect}, allowing it to leverage enhanced node features. -
Applies the provided inline styles (
style) and combines the class names (classNameandclassNames) using theclassnameslibrary. -
Displays a simple greeting message (
Hello {name}!) inside thespan.
5. Export Statement:
export default ExampleComponent;
Exports the ExampleComponent as the default export from the file, making it accessible for use in other parts of the project.
ExampleComponent.config.tsx
The ExampleComponent.config.tsx file provides essential details for crafting, displaying information, and setting default props for the ExampleComponent. It plays a crucial role in defining how the component behaves within the web form editor.
1. Imports:
import { EComponentKind, T4DComponentConfig } from '@ws-ui/webform-editor';
import { Settings } from '@ws-ui/webform-editor';
import { MdOutlineTextSnippet } from 'react-icons/md';
import ExampleComponentSettings, { BasicSettings } from './ExampleComponent.settings';
-
EComponentKindandT4DComponentConfig: Imports types from@ws-ui/webform-editorfor defining component kind and configuration. -
Settings: Imports a function from@ws-ui/webform-editorfor managing component settings. -
MdOutlineTextSnippet: Imports the icon component fromreact-icons/mdfor displaying an icon associated with the component. -
ExampleComponentSettingsandBasicSettingsfrom './ExampleComponent.settings' for additional component settings.
2. Default Export (Configuration Object):
export default {
// ... configuration properties ...
} as T4DComponentConfig<IExampleComponentProps>;
Exports a configuration object as the default export. This object conforms to the T4DComponentConfig type and specifies the configuration for the ExampleComponent with properties related to crafting, information, default props, and more.
3. Craft Configuration:
craft: {
displayName: 'ExampleComponent',
kind: EComponentKind.BASIC,
props: {
name: '',
classNames: [],
events: [],
},
related: {
settings: Settings(ExampleComponentSettings, BasicSettings),
},
},
Defines the crafting configuration for the ExampleComponent:
-
displayName: The display name of the component. -
kind: The kind of component, which is set toEComponentKind.BASIC, categorizes theExampleComponentas aBASICtype within the web form editor. This classification determines its behavior, capabilities, and how it interacts with the editor, providing a foundation for customization and extensibility. -
props: Default values for component props (name,classNames,events). -
related: Establishes a connection to supplementary settings for theExampleComponent. Within therelatedsection, thesettingsproperty employs theSettingsfunction to combineExampleComponentSettings(specific settings) andBasicSettings(common settings). This facilitates a structured and modular strategy for managing different aspects of the component's behavior and visual presentation within the web form editor.
4. Info Configuration:
info: {
displayName: 'ExampleComponent',
exposed: true,
icon: MdOutlineTextSnippet,
events: [
// ... event configurations ...
],
datasources: {
accept: ['string'],
},
},
-
Defines information configuration for the
ExampleComponent:-
displayName: Specifies the display name of the component. -
exposed: Indicates whether the component is exposed or not. -
icon: Specifies the display name of the component. -
events: Defines a list of supported events for the component. -
datasources: Describes the types of data sources accepted by the component.
-
5. Default Props Configuration:
defaultProps: {
name: 'Qodly',
},
Specifies default props for the ExampleComponent. In this case, the default value for the name prop is set to 'Qodly'.
For custom components within iterable structures such as matrices or data tables, incorporating the iterableChild property into the defaultProps in the configuration file can resolve rendering issues. This property enables proper data binding and rendering in these contexts. Update the component's configuration as follows:
export default {
// ... other configurations ...
defaultProps: {
name: 'Qodly',
iterableChild: true
},
}
This addition ensures that custom components function correctly in iterable environments, addressing common data handling and rendering issues.
6. Props Interface:
export interface IExampleComponentProps extends webforms.ComponentProps {
name?: string;
}
Defines an interface IExampleComponentProps that extends webforms.ComponentProps and includes an optional name prop. This interface represents the props that ExampleComponent can receive.
ExampleComponent.render.tsx
The ExampleComponent.render.tsx file defines the rendering logic for the ExampleComponent, incorporating state management, data source access, and dynamic value updates based on changes in the web form editor.
1. Imports:
import { useRenderer, useSources } from '@ws-ui/webform-editor';
import cn from 'classnames';
import { FC, useEffect, useState } from 'react';
import { IExampleComponentProps } from './ExampleComponent.config';
-
useRendereranduseSources: Hooks provided by@ws-ui/webform-editorfor interacting with the web form renderer and accessing data sources. -
cn: A utility for conditionally joining class names. -
FC,useEffect,useState: React utilities for defining functional components, handling side effects, and managing state. -
IExampleComponentProps: Import of the props interface defined inExampleComponent.config.tsx.
2. Functional Component Definition:
const ExampleComponent: FC<IExampleComponentProps> = ({ name, style, className, classNames = [] }) => {
// ... component logic ...
};
Defines a functional component named ExampleComponent that takes props conforming to the IExampleComponentProps interface.
3. Renderer Connection:
const { connect } = useRenderer();
Uses the useRenderer hook to obtain a connect function, which establishes a connection between the component and the web form renderer.
4. State Management:
const [value, setValue] = useState(() => name);
Initializes a state variable value with an initial value derived from the name prop. This state manages the content dynamically displayed by the component.
5. Data Source Access:
const {
sources: { datasource: ds },
} = useSources();
Uses the useSources hook to obtain a data source (ds) from the web form editor. This data source allows dynamic updates to the displayed content.
6. Effect for Data Source Changes:
useEffect(() => {
// ... effect logic ...
}, [ds]);
Utilizes the useEffect hook to perform side effects. In this case, it listens for changes in the data source (ds). The effect runs when ds changes.
7. Data Source Listener Logic:
const listener = async (/* event */) => {
const v = await ds.getValue<string>();
setValue(v || name);
};
Defines a listener function that asynchronously retrieves the current value from the data source (ds) and updates the component state (setValue) accordingly.
8. Effect Cleanup:
return () => {
ds.removeListener('changed', listener);
};
Provides a cleanup function to remove the listener when the component is unmounted, ensuring efficient resource management.
9. Render JSX:
return (
<span ref={connect} style={style} className={cn(className, classNames)}>
Hello {value}!
</span>
);
Returns JSX to render the component. The span element is connected to the web form renderer (ref={connect}) and displays the dynamically updated content (Hello {value}!).
10. Export:
export default ExampleComponent;
Exports the ExampleComponent as the default export from the file, making it available for use in other parts of the project.
ExampleComponent.settings.tsx
The ExampleComponent.settings.tsx file defines the configurable properties of the ExampleComponent. It plays a crucial role in customizing and managing the settings associated with the component within the web form editor.
1. Imports:
import { ESetting, TSetting } from '@ws-ui/webform-editor';
import { BASIC_SETTINGS, DEFAULT_SETTINGS, load } from '@ws-ui/webform-editor';
-
ESettingandTSetting: Imported types from@ws-ui/webform-editorfor defining setting types and configurations. -
BASIC_SETTINGS,DEFAULT_SETTINGS,load: Imported constants and functions from@ws-ui/webform-editorfor managing default and basic settings.
2. Common Settings:
const commonSettings: TSetting[] = [
{
key: 'name',
label: 'Name',
type: ESetting.TEXT_FIELD,
defaultValue: 'Qodly',
},
];
Defines an array commonSettings containing a TEXT_FIELD setting for the component's name. It includes properties such as key, label, type, and defaultValue.
3. Component Settings:
const Settings: TSetting[] = [
{
key: 'properties',
label: 'Properties',
type: ESetting.GROUP,
components: commonSettings,
},
...DEFAULT_SETTINGS,
];
Creates an array Settings that includes a GROUP setting named Properties. This group encapsulates the common settings defined earlier (commonSettings). Additionally, it spreads DEFAULT_SETTINGS to include default settings provided by the webform-editor library.
4. Basic Settings:
export const BasicSettings: TSetting[] = [
...commonSettings,
...load(BASIC_SETTINGS).filter('style.overflow'),
];
Exports an array BasicSettings that includes the common settings and additional settings loaded from BASIC_SETTINGS. It also filters out settings related to style.overflow, providing a subset of basic settings specific to the ExampleComponent.
5. Default Export:
export default Settings;
Exports the Settings array as the default export of the module. This array serves as the complete set of settings for the ExampleComponent, encompassing both common and default settings.