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)
+ }
+}