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')
configuration file. We will see later on this documentation
the purpose of each option:
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()
directory. In this view we
will render a simple profile with an avatar and the user name:
<!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()
directory as defined
in the 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')
,
you may return it from one of your application's routes / controllers
like so:
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')
file, in the 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')
file:
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')
file:
<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')
file. You can find it useful whenever you want to split a large
template file into several smaller ones or also if you want to
provide some templates as part of a npm package:
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')
file. Just make sure to set
the value to 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')
directory. Any change
you make to these files will be reflected when you
generate their correspoding files using Artisan's
make
commands.