From b5bde7d077cb5d229460ef5be5427b9cde272265 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Fri, 7 Aug 2020 09:51:25 -0500 Subject: [PATCH] Add view engine unit - handlebars - and response helpers --- app/bundle/daton_units.ts | 1 + app/http/controllers/Home.controller.ts | 11 ++++ app/http/routes/home.routes.ts | 9 +++ app/http/views/home.hbs | 1 + app/http/views/layouts/.gitkeep | 0 app/http/views/layouts/main.hbs | 14 +++++ app/http/views/partials/.gitkeep | 0 app/units.ts | 2 + lib/src/external/http.ts | 1 + .../response/PartialViewResponseFactory.ts | 18 ++++++ lib/src/http/response/ViewResponseFactory.ts | 19 ++++++ lib/src/http/response/helpers.ts | 10 ++++ lib/src/lifecycle/Application.ts | 21 ++++++- lib/src/unit/ViewEngine.ts | 60 +++++++++++++++++++ 14 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 app/http/controllers/Home.controller.ts create mode 100644 app/http/routes/home.routes.ts create mode 100644 app/http/views/home.hbs create mode 100644 app/http/views/layouts/.gitkeep create mode 100644 app/http/views/layouts/main.hbs create mode 100644 app/http/views/partials/.gitkeep create mode 100644 lib/src/http/response/PartialViewResponseFactory.ts create mode 100644 lib/src/http/response/ViewResponseFactory.ts create mode 100644 lib/src/unit/ViewEngine.ts diff --git a/app/bundle/daton_units.ts b/app/bundle/daton_units.ts index cdf9c38..2d624f5 100644 --- a/app/bundle/daton_units.ts +++ b/app/bundle/daton_units.ts @@ -8,3 +8,4 @@ export { default as ModelsUnit } from '../../orm/src/ModelsUnit.ts' export { default as HttpServerUnit } from '../../lib/src/unit/HttpServer.ts' export { default as RoutingUnit } from '../../lib/src/unit/Routing.ts' export { default as ServicesUnit } from '../../lib/src/unit/Services.ts' +export { default as ViewEngineUnit } from '../../lib/src/unit/ViewEngine.ts' diff --git a/app/http/controllers/Home.controller.ts b/app/http/controllers/Home.controller.ts new file mode 100644 index 0000000..6ceeaa7 --- /dev/null +++ b/app/http/controllers/Home.controller.ts @@ -0,0 +1,11 @@ +import Controller from '../../../lib/src/http/Controller.ts' +import {Request} from '../../../lib/src/http/Request.ts' +import {view} from '../../../lib/src/http/response/helpers.ts' + +export default class HomeController extends Controller { + + get_home(request: Request) { + return view('home', { request }) + } + +} diff --git a/app/http/routes/home.routes.ts b/app/http/routes/home.routes.ts new file mode 100644 index 0000000..c7b0c0d --- /dev/null +++ b/app/http/routes/home.routes.ts @@ -0,0 +1,9 @@ +import { RouterDefinition } from '../../../lib/src/http/type/RouterDefinition.ts' + +export default { + prefix: '/', + middleware: [], + get: { + '/': 'controller::Home.get_home', + }, +} as RouterDefinition diff --git a/app/http/views/home.hbs b/app/http/views/home.hbs new file mode 100644 index 0000000..9eb7a75 --- /dev/null +++ b/app/http/views/home.hbs @@ -0,0 +1 @@ +

Welcome to Daton!

diff --git a/app/http/views/layouts/.gitkeep b/app/http/views/layouts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/http/views/layouts/main.hbs b/app/http/views/layouts/main.hbs new file mode 100644 index 0000000..2129e45 --- /dev/null +++ b/app/http/views/layouts/main.hbs @@ -0,0 +1,14 @@ + + + + + {{#if title}} + {{ title }} | Daton + {{else}} + Daton + {{/if}} + + + {{{ body }}} + + \ No newline at end of file diff --git a/app/http/views/partials/.gitkeep b/app/http/views/partials/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/units.ts b/app/units.ts index 93113a8..faec4f2 100644 --- a/app/units.ts +++ b/app/units.ts @@ -9,6 +9,7 @@ import { HttpServerUnit, RoutingUnit, ServicesUnit, + ViewEngineUnit, } from './bundle/daton_units.ts' export default [ @@ -19,6 +20,7 @@ export default [ HttpKernelUnit, MiddlewareUnit, ControllerUnit, + ViewEngineUnit, RoutesUnit, RoutingUnit, HttpServerUnit, diff --git a/lib/src/external/http.ts b/lib/src/external/http.ts index 527ddb2..6b6409b 100644 --- a/lib/src/external/http.ts +++ b/lib/src/external/http.ts @@ -1,2 +1,3 @@ export * from 'https://deno.land/std@0.53.0/http/server.ts' export * from 'https://deno.land/std@0.53.0/http/cookie.ts' +export { Handlebars } from 'https://deno.land/x/handlebars/mod.ts' \ No newline at end of file diff --git a/lib/src/http/response/PartialViewResponseFactory.ts b/lib/src/http/response/PartialViewResponseFactory.ts new file mode 100644 index 0000000..b8ad437 --- /dev/null +++ b/lib/src/http/response/PartialViewResponseFactory.ts @@ -0,0 +1,18 @@ +import ResponseFactory from './ResponseFactory.ts' +import ViewEngine from '../../unit/ViewEngine.ts' +import {Request} from '../Request.ts' + +export default class PartialViewResponseFactory extends ResponseFactory { + constructor( + public readonly view: string, + public readonly context?: any, + ) { + super() + } + + public async write(request: Request) { + const views: ViewEngine = this.make(ViewEngine) + request.response.body = await views.partial(this.view, this.context) + return request + } +} diff --git a/lib/src/http/response/ViewResponseFactory.ts b/lib/src/http/response/ViewResponseFactory.ts new file mode 100644 index 0000000..8ad6cda --- /dev/null +++ b/lib/src/http/response/ViewResponseFactory.ts @@ -0,0 +1,19 @@ +import ResponseFactory from './ResponseFactory.ts' +import ViewEngine from '../../unit/ViewEngine.ts' +import {Request} from '../Request.ts' + +export default class ViewResponseFactory extends ResponseFactory { + constructor( + public readonly view: string, + public readonly context?: any, + public readonly layout?: string, + ) { + super() + } + + public async write(request: Request) { + const views: ViewEngine = this.make(ViewEngine) + request.response.body = await views.render(this.view, this.context, this.layout) + return request + } +} diff --git a/lib/src/http/response/helpers.ts b/lib/src/http/response/helpers.ts index b94ae21..247566f 100644 --- a/lib/src/http/response/helpers.ts +++ b/lib/src/http/response/helpers.ts @@ -8,6 +8,8 @@ import TemporaryRedirectResponseFactory from './TemporaryRedirectResponseFactory import {HTTPStatus} from '../../const/http.ts' import HTTPErrorResponseFactory from './HTTPErrorResponseFactory.ts' import HTTPError from '../../error/HTTPError.ts' +import ViewResponseFactory from './ViewResponseFactory.ts' +import PartialViewResponseFactory from './PartialViewResponseFactory.ts' export function json(value: any): JSONResponseFactory { return make(JSONResponseFactory, value) @@ -33,3 +35,11 @@ export function redirect(destination: string): TemporaryRedirectResponseFactory export function http(status: HTTPStatus, message?: string): HTTPErrorResponseFactory { return make(HTTPErrorResponseFactory, new HTTPError(status, message)) } + +export function view(view: string, context?: any, layout?: string): ViewResponseFactory { + return make(ViewResponseFactory, view, context, layout) +} + +export function partial(view: string, context?: any): PartialViewResponseFactory { + return make(PartialViewResponseFactory, view, context) +} diff --git a/lib/src/lifecycle/Application.ts b/lib/src/lifecycle/Application.ts index c7749c3..4bb2551 100644 --- a/lib/src/lifecycle/Application.ts +++ b/lib/src/lifecycle/Application.ts @@ -5,8 +5,9 @@ import {container, make} from '../../../di/src/global.ts' import {DependencyKey} from '../../../di/src/type/DependencyKey.ts' import RunLevelErrorHandler from '../error/RunLevelErrorHandler.ts' import {Status} from '../const/status.ts' -import Instantiable from "../../../di/src/type/Instantiable.ts"; -import {Collection} from "../collection/Collection.ts"; +import Instantiable from '../../../di/src/type/Instantiable.ts' +import {Collection} from '../collection/Collection.ts' +import {path} from '../external/std.ts' @Service() export default class Application { @@ -66,4 +67,20 @@ export default class Application { this.logger.verbose(e) } } + + get root() { + return path.resolve('.') + } + + get app_root() { + return path.resolve('./app') + } + + path(...parts: string[]) { + return path.resolve(this.root, ...parts) + } + + app_path(...parts: string[]) { + return path.resolve(this.app_root, ...parts) + } } diff --git a/lib/src/unit/ViewEngine.ts b/lib/src/unit/ViewEngine.ts new file mode 100644 index 0000000..4b96649 --- /dev/null +++ b/lib/src/unit/ViewEngine.ts @@ -0,0 +1,60 @@ +import LifecycleUnit from '../lifecycle/Unit.ts' +import {Unit} from '../lifecycle/decorators.ts' +import {Handlebars} from '../external/http.ts' +import {Logging} from '../service/logging/Logging.ts' +import {fs} from '../external/std.ts' + +@Unit() +export default class ViewEngine extends LifecycleUnit { + protected _handlebars!: Handlebars + + // TODO include basic app info in view data + + constructor( + protected readonly logger: Logging, + ) { + super() + } + + async up() { + this.logger.info(`Setting views base dir: ${this.app.app_path('http', 'views')}`) + this._handlebars = new Handlebars({ + baseDir: this.app.app_path('http', 'views'), + extname: '.hbs', + layoutsDir: 'layouts', + partialsDir: 'partials', + defaultLayout: 'main', + helpers: undefined, + compilerOptions: undefined, + }) + + const main_layout_path = this.app.app_path('http', 'views', 'layouts', 'main.hbs') + if ( !(await fs.exists(main_layout_path)) ) { + this.logger.warn(`Unable to open main view layout file: ${main_layout_path}`) + this.logger.warn(`Unless you are using a custom layout, this could cause errors.`) + } + + const partials_path = this.app.app_path('http', 'views', 'partials') + if ( !(await fs.exists(partials_path)) ) { + this.logger.warn(`Unable to open view partials directory: ${partials_path}`) + this.logger.warn(`This directory must exist for the view engine to function, even if it is empty.`) + } + } + + get handlebars(): Handlebars { + return this._handlebars + } + + async render(view: string, args?: any, layout?: string): Promise { + this.logger.debug(`Rendering view: ${view}`) + return this.handlebars.renderView(view, args, layout) + } + + async partial(view: string, args?: any) { + const parts = `${view}.hbs`.split(':') + const resolved = this.app.app_path('http', 'views', ...parts) + + this.logger.debug(`Rendering partial: ${view} from ${resolved}`) + return this.handlebars.render(resolved, args) + } +}