Views
Understand how you can use the Athenna view API for rendering HTML pages.
Introduction
Of course, it's not practical to return entire HTML documents strings directly from your routes and controllers. Thankfully, views provide a convenient way to place all of our HTML in separate files.
Views separate your controller / application logic from your presentation logic. When using Athenna, view templates are usually written using the Edge templating language. A simple view might look something like this:
<html>
<body>
<h1>Hello, {{ name }}</h1>
</body>
</html>
And we may return it in our routes using the response.view()
method
like so:
Route.get('/', ({ response }) => {
return response.view('greeting', { name: 'Lenon' })
})
Looking for more information on how to write Edge templates? Check out the full Edge documentation to get started.
Views support
Our focus at the moment is to be a framework for backend
applications development. But since the View
API exists in
Athenna ecosystem for creating template files for make
commands
of Artisan, providing this feature for you to create monolith
applications with Athenna is possible, but remember that
supporting this kind of application is not our focus right
now.
If you think this is an interesting feature to improve, feel free to open a discussion at Athenna organization, any contribution is welcome!
Installation
By default, @athenna/view
package will already be installed
in your Athenna project. But you still need to configure it
to be able to use all of it features.
Artisan provides a very simple command to configure the view library in your project. Simply run the following:
node artisan configure @athenna/view
The view configurer will do the following operations in your project:
- Create the
view.ts
configuration file. - Add all view providers in your
.athennarc.json
file. - Add all view commands in your
.athennarc.json
file. - Add all view template files in your
.athennarc.json
file.
Configuration
All the configuration options for your application's views behavior is housed in the
Path.config('view.ts')
./src/config/view.ts
import { Path } from '@athenna/common'
import { Env, Config } from '@athenna/config'
export default {
disk: Path.views(),
namedDisks: {},
components: {},
properties: {
env: Env,
config: Config
},
edge: {
cache: Env('APP_ENV') === 'production'
}
}
Defining & rendering views
To create a new view, use the make:view
command:
node artisan make:view greeting
This command will place a new greeting.edge
template
within your
Path.views()
./src/resources/views
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<h1>{{ name }}</h1>
</body>
</html>
Edge templates contain HTML as well as Edge directives that allow you to easily log values, create "if" statements, iterate over data, and more. Check Edge documentation for more details around the syntax of Edge templates.
Once you have created a view, you may return it from one of your application's
routes or controllers using the response.view()
method:
import { Route } from '@athenna/view'
Route.get('greeting', ({ request, response }) => {
// Render the Path.views('greeting.edge') template
return response.view('greeting', { name: 'Lenon' }) 👈
})
Views may also be rendered using the View
facade:
import { View } from '@athenna/view'
await View.render('greeting')
As you can see, the first argument passed to the method corresponds to the name of the view file in the
Path.views()
./src/resources/views
disk
options of the configuration file
of the view package. The second argument is an object of data that
should be made available to the view. In this case, we are passing
the name
variable, which is displayed in the view using Edge syntax.
Nested view directories
Views may also be nested within subdirectories of your views directory. "Slash" notation may be used to reference nested views. For example, if your view is stored at
Path.views('admin/profile.edge')
./src/resources/views/admin/profile.edge
return response.view('admin/profile', data) 👈
Rendering raw string
You can render raw template string values using the
View.renderRaw()
or View.renderRawSync()
methods:
await View.renderRaw('<p>Hello {{ name }}</p>', {
name: 'Lenon'
})
Raw strings do not enjoy the benefits of template caching as there are not associated with a unique path.
Defining & rendering named disks
Named disks could be defined inside
Path.config('view.ts')
./src/config/view.ts
namedDisks
configuration:
export default {
namedDisks: {
admin: Path.views('admin')
}
}
Now you can render templates by prefixing the disk name before the template path:
return response.view('admin::profile', user) 👈
For rendering nested views inside named disks you can use "slash" syntax:
return response.view('admin::dashboard/home', user) 👈
Sharing data with all views
As you saw in the previous examples, you may pass an object of data to views to make that data available to the view:
return response.view('greeting', { name: 'Lenon' })
But sometimes you may need to share data with all views that are
rendered by your application. You may do so by using the properties
option inside of the
Path.config('view.ts')
./src/config/view.ts
export default {
properties: {
env: Env,
config: Config,
key: 'value',
appName: Env('APP_NAME', '@athenna/athenna')
}
}
You can also use View.addProperty()
method for registering your global
properties:
import { View } from '@athenna/view'
View.addProperty('hello', )
View components
Components are regular views created with the purpose of reuse.
Components can access additional runtime properties like $props
and $slots
, which are unavailable to other views.
We recommend you create components inside the components directory
of your disk
root path. This helps create a visual boundary between
the components and the rest of the templates used by your application.
Let's start by creating a button component. We will store it inside the
Path.views('components/button.edge')
./src/resources/views/components/button.edge
<button type="{{ type || 'submit' }}">{{ text }}</button>
Using components
You must use the @component
tag to render a component inside your
templates. The component tag accepts the template path as the first
parameter and props as the second parameter:
<form>
@!component('components/button', { text: 'Login' })
@!component('components/button', { text: 'Cancel', type: 'reset' })
</form>
Output:
<form>
<button type="submit">Login</button>
<button type="reset">Cancel</button>
</form>
Components from named disks can be referenced by prefixing the disk name:
@!component('uikit::components/button', { text: 'Login' })
For more information about components, check Edge component documentation page.
In-memory components
You can register in-memory views components by defining them
in the components
property of you
Path.config('view.ts')
./src/config/view.ts
export default {
components: {
'myuikit/button': Path.node_modules('myuikit/button.edge')
}
}
You can also create in-memory components without defining any file
using the View
facade directly:
import { View } from '@athenna/view'
View.createComponent('myuikit/button', '<button {{ $props.serializeExcept(["title"]) }}>{{ title }}</button>')
You can also use the View.createTemplate()
method to
register templates. The only difference between them is
that View.createTemplate()
does not throws errors if
some template name already exists, instead the old template
will be replaced.
You can render the template directly or use it as a component in other views:
@!component('myuikit/button', {
title: 'Signup',
class: ['btn', 'btn-primary'],
id: 'signup'
})
Optimizing views
Compiling a template to a JavaScript function is a time-consuming process, and hence it is recommended to cache the compiled templates in production.
You can control the template caching using the edge.cache
property inside
Path.config('view.ts')
./src/config/view.ts
true
in the production environment:
export default {
edge: {
// Set to `true` in production only
cache: Env('APP_ENV') === 'production' 👈
}
}
All the templates are cached within the memory. Currently, we do not have any plans to support on-disk caching since the value provided for the efforts is too low.
The raw text does not take up too much space, and even keeping thousands of pre-compiled templates in memory should not be a problem.
More details about views syntax
If you want to know more informations about the views syntax like loops, "if" statements and declaring properties, check out the full Edge documentation for all the details around it syntax.
Customizing make
commands templates
The Artisan console's make
commands are used to create
a variety of classes, such as controllers, services,
commands, and tests. These classes are generated using
.edge
files that are populated with values based on
your input. However, you may want to make small changes
to files generated by Artisan. To accomplish this, you
may use the template:customize
command to publish the
most common stubs to your application so that you can
customize them:
node artisan template:customize
The customized templates will be located in the
Path.resources('templates')
./resources/templates
make
commands.