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
react
plugin is included to enable React support. - The custom
redirect
plugin 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
,proxy
settings, 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
andonProxyRes
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:
onProxyReq
is an event triggered when a request is being sent to the target server through the proxy. It takes as parameters:proxyReq
is the HTTP request object being sent to the target server.req
is 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-8
for proper character handling and attaches the assembledbody
toreq.body
.Additional headers are set on the
proxyReq
object 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:
onProxyRes
is an event triggered when a response is received from the target server through the proxy. It takesproxyRes
as 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
sanitizeSetCookie
function to each element usingmap
.If 'Set-Cookie' is not an array, it applies the
sanitizeSetCookie
function directly.
The
sanitizeSetCookie
function is responsible for removing the 'secure' flag from the 'Set-Cookie' header, ensuring it is compatible with non-secure connections.infoThis 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
root
is 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
WebformEditorStandalone
component is imported from the@ws-ui/webform-editor-standalone
library, serving as a specialized editor for web forms.The
components
import from the./components
directory 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-editor
for handling enhanced editor features.Build
andRender
: Import components responsible for building and rendering theExampleComponent
.
2. Component Declaration:
const ExampleComponent: T4DComponent<IExampleComponentProps> = (props) => {
Declares a constant variable
ExampleComponent
of typeT4DComponent<IExampleComponentProps>
, indicating that it is a custom component for the Page Editor.The component is a functional component that receives
props
of typeIExampleComponentProps
.
3. Enhanced Editor State Check:
const { enabled } = useEnhancedEditor((state) => ({
enabled: state.options.enabled,
}));
Utilizes the
useEnhancedEditor
hook to access the enhanced editor state.The
state
object is destructured to obtain theenabled
property, 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
enabled
istrue
, it renders theBuild
component, passing theprops
to it using the spread operator ({...props}
).If
enabled
isfalse
, it renders theRender
component 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-editor
for handling enhanced node features.cn
: Imports theclassnames
library, which is used for conditionally applying class names.FC
: Imports theFC
(Functional Component) type from React.IExampleComponentProps
: Imports the props interface specific toExampleComponent
from the configuration file.
2. Component Definition:
const ExampleComponent: FC<IExampleComponentProps> = ({ name, style, className, classNames = [] }) => {
Declares a constant variable
ExampleComponent
as a functional component (FC
) that takesIExampleComponentProps
as 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
span
element 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 (className
andclassNames
) using theclassnames
library.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';
EComponentKind
andT4DComponentConfig
: Imports types from@ws-ui/webform-editor
for defining component kind and configuration.Settings
: Imports a function from@ws-ui/webform-editor
for managing component settings.MdOutlineTextSnippet
: Imports the icon component fromreact-icons/md
for displaying an icon associated with the component.ExampleComponentSettings
andBasicSettings
from './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 to `EComponentKind.BASIC`, categorizes the `ExampleComponent` as a `BASIC` type 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 the `ExampleComponent`. Within the `related` section, the `settings` property employs the `Settings` function to combine `ExampleComponentSettings` (specific settings) and `BasicSettings` (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';
useRenderer
anduseSources
: Hooks provided by@ws-ui/webform-editor
for 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';
ESetting
andTSetting
: Imported types from@ws-ui/webform-editor
for defining setting types and configurations.BASIC_SETTINGS
,DEFAULT_SETTINGS
,load
: Imported constants and functions from@ws-ui/webform-editor
for 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.