Skip to content

Commit

Permalink
Add custom "onRender" event handler
Browse files Browse the repository at this point in the history
  • Loading branch information
ahmadnasriya committed Jul 17, 2024
1 parent 2620cc2 commit 639fcb2
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 52 deletions.
24 changes: 23 additions & 1 deletion examples/server-side-rendering-ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,33 @@ import { Component } from '@nasriya/hypercloud';
const comp = new Component('header'); // Where `header` is the component name
```

3. Specify the path of the component template
3. Set either a component template or a `render` handler.
```js
// Set a templete
comp.template.path.set(path.join(import.meta.dirname, 'header.ejs'));

// Set a rendering handler
comp.onRender.set(locals => {
return `
<header>
<img src="${locals.logo.src}">
<nav>
<ul>
${locals.nav.map(nav => {
return `<a href="${nav.href ? nav.href : '#'}"><li>${nav.label}</li></a>`
})}
</ul>
</nav>
</header>
`
})
```
**Notes:**
- Only one method is required in order to render the component.
- If both the `onRender` handler and a `template` have been set, the `template` will be ignored.
- The `onRenderHandler` should either return a `string` or `Promise<string>`.
4. Specify the paths of the assets (optional)
```js
comp.stylesheet.set(path.join(import.meta.dirname, 'style.css'));
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nasriya/hypercloud",
"version": "1.0.3",
"version": "1.0.4",
"description": "Nasriya HyperCloud is a lightweight Node.js HTTP2 framework.",
"main": "./dist/cjs/hypercloud.js",
"module": "./dist/esm/hypercloud.js",
Expand Down
2 changes: 2 additions & 0 deletions src/docs/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export type MimeType =
| "application/vnd.rar" | "application/rtf" | "application/x-sh" | "image/svg+xml"
| "application/x-tar" | "image/tiff";

export type OnRenderHandler = (locals: Record<string, any> | any, include: (name: string, locals: Record<string, any>) => Promise<string>, lang: string) => string | Promise<string>;

export interface HTMLScriptTag {
/**
* Specifies that the script is downloaded in parallel to
Expand Down
3 changes: 3 additions & 0 deletions src/hypercloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import path from 'path';
import process from 'process';
export { Page } from './services/renderer/assets/Page';
export { Component } from './services/renderer/assets/Component';
export { HyperCloudServer } from './server';
export { HyperCloudRequest } from './services/handler/assets/request';
export { HyperCloudResponse } from './services/handler/assets/response';

process.env.HYPERCLOUD_SERVER_VERBOSE = 'FALSE';

Expand Down
3 changes: 2 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import LanguagesManager from './services/languages/manager';
const _dirname = __dirname;

/**HyperCloud HTTP2 server */
class HyperCloudServer {
export class HyperCloudServer {
#_recievedReqNum = 0;
readonly #_system = {
httpServer: undefined as http.Server | undefined,
Expand Down Expand Up @@ -585,4 +585,5 @@ class HyperCloudServer {
return this;
}
}

export default HyperCloudServer;
2 changes: 1 addition & 1 deletion src/services/handler/assets/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import helpers from '../../../utils/helpers';
import HyperCloudUser from './user';

/**This class is used internallly, not by the user */
class HyperCloudRequest {
export class HyperCloudRequest {
readonly #_request: InitializedRequest;
readonly #_req: http2.Http2ServerRequest;
#_params = {} as Record<string, string>;
Expand Down
16 changes: 8 additions & 8 deletions src/services/handler/assets/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ type EventCallback = (...args: any[]) => void;
* TODO: Change all the server examples to use my own server class
*/

/**This class is used internallly, not by the user */
class HyperCloudResponse {
/**This class is used internally, not by the user */
export class HyperCloudResponse {
readonly #_server: HyperCloudServer;
readonly #_req: HyperCloudRequest;
readonly #_res: http2.Http2ServerResponse;
Expand Down Expand Up @@ -107,7 +107,7 @@ class HyperCloudResponse {
* })
* @param {NotFoundResponseOptions} [options] Rendering options
*/
notFound: (options?: NotFoundResponseOptions) => {
notFound: async (options?: NotFoundResponseOptions) => {
try {
if (typeof this.#_server._handlers.notFound === 'function') {
try {
Expand Down Expand Up @@ -175,7 +175,7 @@ class HyperCloudResponse {
* })
* @param {ForbiddenAndUnauthorizedOptions} [options]
*/
unauthorized: (options?: ForbiddenAndUnauthorizedOptions) => {
unauthorized: async (options?: ForbiddenAndUnauthorizedOptions) => {
try {
if (typeof this.#_server._handlers.unauthorized === 'function') {
try {
Expand Down Expand Up @@ -256,7 +256,7 @@ class HyperCloudResponse {
* })
* @param {ForbiddenAndUnauthorizedOptions} options
*/
forbidden: (options?: ForbiddenAndUnauthorizedOptions) => {
forbidden: async (options?: ForbiddenAndUnauthorizedOptions) => {
try {
if (typeof this.#_server._handlers.forbidden === 'function') {
try {
Expand Down Expand Up @@ -327,7 +327,7 @@ class HyperCloudResponse {
* })
* @param {ServerErrorOptions} options
*/
serverError: (options?: ServerErrorOptions) => {
serverError: async (options?: ServerErrorOptions) => {
try {
if (options && 'error' in options) {
const dashLine = '#'.repeat(50);
Expand Down Expand Up @@ -425,10 +425,10 @@ class HyperCloudResponse {
* @param {PageRenderingOptions} options
* @returns {HyperCloudResponse}
*/
render(name: string, options?: PageRenderingOptions): HyperCloudResponse {
async render(name: string, options?: PageRenderingOptions): Promise<HyperCloudResponse> {
try {
const renderer = new Renderer(this.#_req, name);
const html = renderer.render(options);
const html = await renderer.render(options);
this.setHeader('Content-Type', 'text/html');

if (options && 'httpOptions' in options) {
Expand Down
36 changes: 26 additions & 10 deletions src/services/renderer/assets/Component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FileAsset, InternalScriptOptions, InternalScriptRecord, InternalStylesheetRecord, PageRenderingCacheAsset, ViewRenderingAsset } from '../../../docs/docs';
import { FileAsset, InternalScriptOptions, InternalScriptRecord, InternalStylesheetRecord, OnRenderHandler, PageRenderingCacheAsset, ViewRenderingAsset } from '../../../docs/docs';
import helpers from '../../../utils/helpers';
import fs from 'fs';
import path from 'path';
Expand All @@ -7,7 +7,7 @@ export class Component {
readonly #_id = helpers.generateRandom(16, { includeSymbols: false });
/**Component name */
readonly #_name: string;
/**The EJS template */
/**The template */
readonly #_template: ViewRenderingAsset = { filePath: '', content: '' }

/**The component's stylesheet */
Expand Down Expand Up @@ -76,6 +76,8 @@ export class Component {
}
}

#_onRender: OnRenderHandler | null = null;

/**
* Create a new component
* @param name The component name
Expand Down Expand Up @@ -107,9 +109,9 @@ export class Component {
set: (filePath: string) => {
const validity = helpers.checkPathAccessibility(filePath);
if (!validity.valid) {
if (!validity.errors.notString) { throw this.#_helpers.createError(`The stylesheet path that you passed should be a string, instead got ${typeof filePath}`) }
if (!validity.errors.doesntExist) { throw this.#_helpers.createError(`The stylesheet path (${filePath}) doesn't exist.`) }
if (!validity.errors.notAccessible) { throw this.#_helpers.createError(`You don't have enough permissions to access the stylesheet path (${filePath})`) }
if (validity.errors.notString) { throw this.#_helpers.createError(`The stylesheet path that you passed should be a string, instead got ${typeof filePath}`) }
if (validity.errors.doesntExist) { throw this.#_helpers.createError(`The stylesheet path (${filePath}) doesn't exist.`) }
if (validity.errors.notAccessible) { throw this.#_helpers.createError(`You don't have enough permissions to access the stylesheet path (${filePath})`) }
}

const name = path.basename(filePath);
Expand Down Expand Up @@ -184,7 +186,7 @@ export class Component {
}
}
},
get: (lang: string = 'default') => this.#_locals[lang] || this.#_locals.default
get: (lang: string = 'default') => lang in this.#_locals ? this.#_locals[lang] : this.#_locals.default
}

/**Component name */
Expand Down Expand Up @@ -223,11 +225,11 @@ export class Component {
},
/**
* Disable caching for this component.
* @param extensions The extensions you want to disble. Default: All assets
* @param extensions The extensions you want to disable. Default: All assets
* @example
* component.cache.disble(); // Disable caching for all assets
* component.cache.disble('js'); // Disable caching for JavaScript Files
* component.cache.disble(['js', 'css']); // Disable caching for CSS Files
* component.cache.disable(); // Disable caching for all assets
* component.cache.disable('js'); // Disable caching for JavaScript Files
* component.cache.disable(['js', 'css']); // Disable caching for CSS Files
*/
disable: (extensions?: PageRenderingCacheAsset | PageRenderingCacheAsset[]) => {
try {
Expand Down Expand Up @@ -255,6 +257,7 @@ export class Component {
*/
update: async () => {
try {
if (!this.#_template.filePath) { return }
const promises: Promise<void>[] = [new Promise<void>((resolve, reject) => {
try {
this.#_template.content = fs.readFileSync(this.#_template.filePath, { encoding: 'utf-8' });
Expand Down Expand Up @@ -310,6 +313,19 @@ export class Component {
/**Read the caching status of this component */
status: () => this.#_cache.extensions
}

readonly onRender = {
/**
* Set the component's renderer function
* @param {OnRenderHandler} callback
*/
set: (callback: OnRenderHandler) => {
if (typeof callback !== 'function') { throw new TypeError(`The component's "onRender" handler must be a function, instead got ${typeof callback}`) }
this.#_onRender = callback;
},
/**Get the component's render function */
get: () => { return this.#_onRender }
}
}

export default Component;
18 changes: 9 additions & 9 deletions src/services/renderer/assets/Page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class Page {
#_id = helpers.generateRandom(16, { includeSymbols: false });
/**Page name (internally on the server) */
readonly #_name: string;
/**The EJS template */
/**The template */
readonly #_template: ViewRenderingAsset = { filePath: '', content: '' }

readonly #_title: Record<string, string> = { default: '' }
Expand Down Expand Up @@ -335,7 +335,7 @@ export class Page {
}
}
},
get: (lang: string = 'default') => this.#_locals[lang] || this.#_locals.default
get: (lang: string = 'default') => lang in this.#_locals ? this.#_locals[lang] : this.#_locals.default
}

readonly scripts = {
Expand Down Expand Up @@ -422,9 +422,9 @@ export class Page {
internal: (filePath: string) => {
const validity = helpers.checkPathAccessibility(filePath);
if (!validity.valid) {
if (!validity.errors.notString) { throw this.#_helpers.createError(`The stylesheet path that you passed should be a string, instead got ${typeof filePath}`) }
if (!validity.errors.doesntExist) { throw this.#_helpers.createError(`The stylesheet path (${filePath}) doesn't exist.`) }
if (!validity.errors.notAccessible) { throw this.#_helpers.createError(`You don't have enough permissions to access the stylesheet path (${filePath})`) }
if (validity.errors.notString) { throw this.#_helpers.createError(`The stylesheet path that you passed should be a string, instead got ${typeof filePath}`) }
if (validity.errors.doesntExist) { throw this.#_helpers.createError(`The stylesheet path (${filePath}) doesn't exist.`) }
if (validity.errors.notAccessible) { throw this.#_helpers.createError(`You don't have enough permissions to access the stylesheet path (${filePath})`) }
}

const name = path.basename(filePath);
Expand Down Expand Up @@ -480,11 +480,11 @@ export class Page {
},
/**
* Disable caching for this page.
* @param extensions The extensions you want to disble. Default: All assets
* @param extensions The extensions you want to disable. Default: All assets
* @example
* page.cache.disble(); // Disable caching for all assets
* page.cache.disble('js'); // Disable caching for JavaScript Files
* page.cache.disble(['js', 'css']); // Disable caching for CSS Files
* page.cache.disable(); // Disable caching for all assets
* page.cache.disable('js'); // Disable caching for JavaScript Files
* page.cache.disable(['js', 'css']); // Disable caching for CSS Files
*/
disable: (extensions?: PageRenderingCacheAsset | PageRenderingCacheAsset[]) => {
try {
Expand Down
2 changes: 1 addition & 1 deletion src/services/renderer/managers/componentsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class ComponentsManager {
*/
register(paths: string | string[]) {
if (!Array.isArray(paths)) { paths = [paths] }
const errRes = { message: 'Invalid components\' paths detected. Read the error list', errors: [] as any[] }
const errRes = { message: 'Invalid components\' paths detected. Read the error list', errors: [] as any[], paths }

// Validating input
for (const viewsPath of paths) {
Expand Down
8 changes: 4 additions & 4 deletions src/services/renderer/managers/globalAssetsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,9 @@ class GlobalAssets {
internal: (filePath: string) => {
const validity = helpers.checkPathAccessibility(filePath);
if (!validity.valid) {
if (!validity.errors.notString) { throw this.#_helpers.createError(`The stylesheet path that you passed should be a string, instead got ${typeof filePath}`) }
if (!validity.errors.doesntExist) { throw this.#_helpers.createError(`The stylesheet path (${filePath}) doesn't exist.`) }
if (!validity.errors.notAccessible) { throw this.#_helpers.createError(`You don't have enough permissions to access the stylesheet path (${filePath})`) }
if (validity.errors.notString) { throw this.#_helpers.createError(`The stylesheet path that you passed should be a string, instead got ${typeof filePath}`) }
if (validity.errors.doesntExist) { throw this.#_helpers.createError(`The stylesheet path (${filePath}) doesn't exist.`) }
if (validity.errors.notAccessible) { throw this.#_helpers.createError(`You don't have enough permissions to access the stylesheet path (${filePath})`) }
}

const name = path.basename(filePath);
Expand Down Expand Up @@ -326,7 +326,7 @@ class GlobalAssets {
* @returns
*/
get: (lang: string = 'default'): Record<string, any> => {
return this.#_locals[lang] || this.#_locals.default;
return lang in this.#_locals ? this.#_locals[lang] : this.#_locals.default;
}
}

Expand Down
Loading

0 comments on commit 639fcb2

Please sign in to comment.