Script Blocks
Script Blocks lets you define reusable statements that can be invoked with a new context allowing the creation custom iterators and helpers - making it easy to encapsulate reusable functionality and reduce boilerplate for common functionality.
Default Blocks
name | body |
---|---|
noop | verbatim |
with | default |
if | default |
while | default |
raw | verbatim |
function | code |
defn | lisp |
capture | template |
markdown | verbatim |
csv | verbatim |
partial | template |
html | template |
ServiceStack Blocks
name | body |
---|---|
minifyjs | verbatim |
minifycss | verbatim |
minifyhtml | verbatim |
svg | template |
Script Block Body
The Body
of the Script Block specifies how the body is evaluated. Script Blocks can be used in both #Script
Template and
Code Statement Blocks.
Below are examples of using a script block of each body type in both #Script
template and code statement blocks:
default
If unspecified Script Blocks are evaluated within the language they're used within.
By default #Script
pages use template expression handlebars syntax:
Whilst in Code Statement Blocks the body is executed as JS Expressions, which requires using quoted strings or template literals for any text you want to emit, e.g:
```code
#if test.isEven()
`${test} is even`
else
`${test} is odd`
/if
```
verbatim
The contents of verbatim script blocks are unprocessed and evaluated as raw text by the script block:
```code
#csv cars
Tesla,Model S,79990
Tesla,Model 3,38990
Tesla,Model X,84990
/csv
```
template
The contents of template Script Blocks are processed using Template Expression syntax:
```code
#capture out
{{#each range(3)}}
- {{it + 1}}
{{/each}}
/capture
```
code
The contents of code Script Blocks are processed as JS Expression statements:
```code
#function calc(a, b)
a * b |> to => c
a + b + c |> return
/function
```
lisp
Finally the contents of lisp Script Blocks is processed by #Script Lisp:
```code
#defn calc [a, b]
(def c (* a b))
(+ a b c)
/defn
```
Syntax
The syntax for blocks follows the familiar handlebars block helpers in both syntax and functionality.
#Script
also includes most of handlebars.js block helpers which are useful in a HTML template language whilst minimizing any porting efforts if
needing to reuse existing JavaScript handlebars templates.
We'll walk through creating a few of the built-in Script blocks to demonstrate how to create them from scratch.
noop
We'll start with creating the noop
block (short for "no operation") which functions like a block comment by removing its inner contents
from the rendered page:
<div class="entry">
<h1>{{title}}</h1>
<div class="body">
{{#noop}}
<h2>Removed Content</h2>
{{page}}
{{/noop}}
</div>
</div>
The noop
block is also the smallest implementation possible which needs to inherit ScriptBlock
class, overrides the Name
getter with
the name of the block and implements the WriteAsync()
method which for the noop block just returns an empty Task
there by not writing anything
to the Output Stream, resulting in its inner contents being ignored:
public class NoopScriptBlock : ScriptBlock
{
public override string Name => "noop";
public override Task WriteAsync(ScriptScopeContext scope, PageBlockFragment block, CancellationToken ct)
=> Task.CompletedTask;
}
All Block's are executed with 3 parameters:
ScriptScopeContext
- The current Execution and Rendering contextPageBlockFragment
- The parsed Block contentsCancellationToken
- Allows the async render operation to be cancelled
Registering Blocks
The same flexible registration options for Registering Script Methods is also available for registering blocks
where if it wasn't already built-in, NoopScriptBlock
could be registered by adding it to the ScriptBlocks
collection:
var context = new ScriptContext {
ScriptBlocks = { new NoopScriptBlock() },
}.Init();
Autowired using ScriptContext IOC
Autowired instances of script blocks and methods can also be created using ScriptContext's configured IOC where they're also injected with any
registered IOC dependencies by registering them in the ScanTypes
collection:
var context = new ScriptContext
{
ScanTypes = { typeof(NoopScriptBlock) }
};
context.Container.AddSingleton<ICacheClient>(() => new MemoryCacheClient());
context.Init();
When the ScriptContext
is initialized it will go through each Type and create an autowired instance of each Type and register them in the
ScriptBlocks
collection. An alternative to registering individual Types is to register an entire Assembly, e.g:
var context = new ScriptContext
{
ScanAssemblies = { typeof(MyBlock).Assembly }
};
Where it automatically registers any Script Blocks or Methods contained in the Assembly where the MyBlock
Type is defined.
bold
A step up from noop
is the bold Script Block which markup its contents within the <b/>
tag:
{{#bold}}This text will be bold{{/bold}}
Which calls the base.WriteBodyAsync()
method to evaluate and write the Block's contents to the OutputStream
using the current
ScriptScopeContext
:
public class BoldScriptBlock : ScriptBlock
{
public override string Name => "bold";
public override async Task WriteAsync(
ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
{
await scope.OutputStream.WriteAsync("<b>", token);
await WriteBodyAsync(scope, block, token);
await scope.OutputStream.WriteAsync("</b>", token);
}
}
with
The with
Block shows an example of utilizing arguments. To maximize flexibility arguments passed into your block are captured in a free-form
string (specifically a ReadOnlyMemory<char>
) which allows creating Blocks varying from simple arguments to complex LINQ-like expressions - a
feature some built-in Blocks take advantage of.
The with
block works similarly to handlebars with helper or JavaScript's
with statement where it extracts the properties (or Keys)
of an object and adds them to the current scope which avoids needing a prefix each property reference,
e.g. being able to use {{Name}}
instead of {{person.Name}}
:
{{#with person}} Hi {{Name}}, your Age is {{Age}}. {{/with}}
Also the with
Block's contents are only evaluated if the argument expression is null
.
The implementation below shows the optimal way to implement with
by calling GetJsExpressionAndEvaluate()
to resolve a cached
AST token that's then evaluated to return the result of the Argument expression.
If the argument evaluates to an object it calls the ToObjectDictionary()
extension method to convert it into a Dictionary<string,object>
then creates a new scope with each property added as arguments and then evaluates the block's Body contents with the new scope:
public class WithScriptBlock : ScriptBlock
{
public override string Name => "with";
public override async Task WriteAsync(
ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
{
var result = block.Argument.GetJsExpressionAndEvaluate(scope,
ifNone: () => throw new NotSupportedException("'with' block does not have a valid expression"));
if (result != null)
{
var resultAsMap = result.ToObjectDictionary();
var withScope = scope.ScopeWithParams(resultAsMap);
await WriteBodyAsync(withScope, block, token);
}
}
}
To better highlight how this works, a non-cached version of GetJsExpressionAndEvaluate()
involves parsing the Argument string into
an AST Token then evaluating it with the current scope:
block.Argument.ParseJsExpression(out token);
var result = token.Evaluate(scope);
The ParseJsExpression()
extension method is able to parse virtually any JavaScript Expression into an AST tree
which can then be evaluated by calling its token.Evaluate(scope)
method.
Final implementation
The actual WithScriptBlock.cs
used in #Script includes extended functionality which uses GetJsExpressionAndEvaluateAsync()
to be able to evaluate both sync and async
results.
else if/else statements
It also evaluates any block.ElseBlocks
statements which is functionality available to all blocks which are able to evaluate any alternative
else/else if statements when the main template isn't rendered, e.g. in this case when the with
block is called with a null
argument:
public class WithScriptBlock : ScriptBlock
{
public override string Name => "with";
public override async Task WriteAsync(
ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
{
var result = await block.Argument.GetJsExpressionAndEvaluateAsync(scope,
ifNone: () => throw new NotSupportedException("'with' block does not have a valid expression"));
if (result != null)
{
var resultAsMap = result.ToObjectDictionary();
var withScope = scope.ScopeWithParams(resultAsMap);
await WriteBodyAsync(withScope, block, token);
}
else
{
await WriteElseAsync(scope, block.ElseBlocks, token);
}
}
}
This enables the with
block to also evaluate async responses like the async results returned in async Database scripts,
it's also able to evaluate custom else statements for rendering different results based on alternate conditions, e.g:
if
Since all blocks are able to execute any number of {{else}}
statements by calling base.WriteElseAsync()
, the implementation for
the #if
block ends up being even simpler which just needs to evaluate the argument to bool
.
If true it writes the body with WriteBodyAsync()
otherwise it evaluates any else
statements with WriteElseAsync()
:
/// <summary>
/// Handlebars.js like if block
/// Usages: {{#if a > b}} max {{a}} {{/if}}
/// {{#if a > b}} max {{a}} {{else}} max {{b}} {{/if}}
/// {{#if a > b}} max {{a}} {{else if b > c}} max {{b}} {{else}} max {{c}} {{/if}}
/// </summary>
public class IfScriptBlock : ScriptBlock
{
public override string Name => "if";
public override async Task WriteAsync(
ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
{
var result = await block.Argument.GetJsExpressionAndEvaluateToBoolAsync(scope,
ifNone: () => throw new NotSupportedException("'if' block does not have a valid expression"));
if (result)
{
await WriteBodyAsync(scope, block, token);
}
else
{
await WriteElseAsync(scope, block.ElseBlocks, token);
}
}
}
while
Similar to #if
, the #while
block takes a boolean expression, except it keeps evaluating its body until the expression evaluates to false
.
The implementation includes a safe-guard to ensure it doesn't exceed the configured ScriptContext.MaxQuota
to avoid infinite recursion:
/// <summary>
/// while block
/// Usages: {{#while times > 0}} {{times}}. {{times - 1 |> to => times}} {{/while}}
/// {{#while b}} {{ false |> to => b }} {{else}} {{b}} was false {{/while}}
///
/// Max Iterations = Context.Args[ScriptConstants.MaxQuota]
/// </summary>
public class WhileScriptBlock : ScriptBlock
{
public override string Name => "while";
public override async Task WriteAsync(
ScriptScopeContext scope, PageBlockFragment block, CancellationToken ct)
{
var result = await block.Argument.GetJsExpressionAndEvaluateToBoolAsync(scope,
ifNone: () => throw new NotSupportedException("'while' block is not valid"));
var iterations = 0;
if (result)
{
do
{
await WriteBodyAsync(scope, block, ct);
result = await block.Argument.GetJsExpressionAndEvaluateToBoolAsync(scope,
ifNone: () => throw new NotSupportedException("'while' block is not valid"));
Context.DefaultMethods.AssertWithinMaxQuota(iterations++);
} while (result);
}
else
{
await WriteElseAsync(scope, block.ElseBlocks, ct);
}
}
}
each
From what we've seen up till now, the handlebars.js each block is also
straightforward to implement which just iterates over a collection argument that evaluates its body with a new scope containing the
elements properties, a conventional it
binding for the element and an index
argument that can be used to determine the
index of each element:
/// <summary>
/// Handlebars.js like each block
/// Usages: {{#each customers}} {{Name}} {{/each}}
/// {{#each customers}} {{it.Name}} {{/each}}
/// {{#each customers}} Customer {{index + 1}}: {{Name}} {{/each}}
/// {{#each numbers}} {{it}} {{else}} no numbers {{/each}}
/// {{#each numbers}} {{it}} {{else if letters != null}} has letters {{else}} no numbers {{/each}}
/// </summary>
public class SimpleEachScriptBlock : ScriptBlock
{
public override string Name => "each";
public override async Task WriteAsync(
ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
{
var collection = (IEnumerable) block.Argument.GetJsExpressionAndEvaluate(scope,
ifNone: () => throw new NotSupportedException("'each' block does not have a valid expression"));
var index = 0;
if (collection != null)
{
foreach (var element in collection)
{
var scopeArgs = element.ToObjectDictionary();
scopeArgs["it"] = element;
scopeArgs[nameof(index)] = index++;
var itemScope = scope.ScopeWithParams(scopeArgs);
await WriteBodyAsync(itemScope, block, token);
}
}
if (index == 0)
{
await WriteElseAsync(scope, block.ElseBlocks, token);
}
}
}
Despite its terse implementation, the above Script Block can be used to iterate over any expression that evaluates to a collection, inc. objects, POCOs, strings as well as Value Type collections like ints.
Built-in each
However the built-in EachScriptBlock.cs has a larger implementation to support its richer feature-set where it also includes support for async results, custom element bindings and LINQ-like syntax for maximum expressiveness whilst utilizing expression caching to ensure any complex argument expressions are only parsed once.
/// <summary>
/// Handlebars.js like each block
/// Usages: {{#each customers}} {{Name}} {{/each}}
/// {{#each customers}} {{it.Name}} {{/each}}
/// {{#each num in numbers}} {{num}} {{/each}}
/// {{#each num in [1,2,3]}} {{num}} {{/each}}
/// {{#each numbers}} {{it}} {{else}} no numbers {{/each}}
/// {{#each numbers}} {{it}} {{else if letters != null}} has letters {{else}} no numbers {{/each}}
/// {{#each n in numbers where n > 5}} {{it}} {{else}} no numbers > 5 {{/each}}
/// {{#each n in numbers where n > 5 orderby n skip 1 take 2}} {{it}} {{else}}no numbers > 5{{/each}}
/// </summary>
public class EachScriptBlock : ScriptBlock { ... }
By using ParseJsExpression()
to parse expressions after each "LINQ modifier", each
supports evaluating complex JavaScript expressions in each
of its LINQ querying features, e.g:
Custom bindings
When using a custom binding like {{#each c in customers}}
above, the element is only accessible with the custom c
binding which is more efficient
when only needing to reference a subset of the element's properties as it avoids adding each of the elements properties in the items execution scope.
Check out LINQ Examples for more live previews showcasing advanced usages of the {{#each}}
block.
raw
The {{#raw}}
block is similar to handlebars.js's raw-helper which captures
the template's raw text content instead of having its content evaluated, making it ideal for emitting content that could contain
template expressions like client-side JavaScript or template expressions that shouldn't be evaluated on the server such as
Vue, Angular or Ember templates:
{{#raw}} <div id="app"> {{ message }} </div> {{/raw}}
When called with no arguments it will render its unprocessed raw text contents. When called with a single argument, e.g. {{#raw varname}}
it will
instead save the raw text contents to the specified global PageResult
variable and lastly when called with the appendTo
modifier it will append
its contents to the existing variable, or initialize it if it doesn't exist.
This is now the preferred approach used in all .NET Core and .NET Framework Web Templates for pages and partials to append any custom JavaScript script blocks they need on the page, e.g:
{{#raw appendTo scripts}} <script> //... </script> {{/raw}}
Where any captured custom scripts are rendered at the bottom of _layout.html with:
<script src="/assets/js/default.js"></script> {{ scripts |> raw }} </body> </html>
The implementation to support each of these usages is contained within
RawScriptBlock.cs
below which inspects the block.Argument
to determine whether it should capture the contents into the specified variable or write its raw
string contents directly to the OutputStream:
/// <summary>
/// Special block which captures the raw body as a string fragment
///
/// Usages: {{#raw}}emit {{ verbatim }} body{{/raw}}
/// {{#raw varname}}assigned to varname{{/raw}}
/// {{#raw appendTo varname}}appended to varname{{/raw}}
/// </summary>
public class RawScriptBlock : ScriptBlock
{
public override string Name => "raw";
public override ScriptLanguage Body => ScriptVerbatim.Language;
public override async Task WriteAsync(
ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
{
var strFragment = (PageStringFragment)block.Body[0];
if (!block.Argument.IsNullOrWhiteSpace())
{
Capture(scope, block, strFragment);
}
else
{
await scope.OutputStream.WriteAsync(strFragment.Value.Span, token);
}
}
private static void Capture(
ScriptScopeContext scope, PageBlockFragment block, PageStringFragment strFragment)
{
var literal = block.Argument.Span.AdvancePastWhitespace();
bool appendTo = false;
if (literal.StartsWith("appendTo "))
{
appendTo = true;
literal = literal.Advance("appendTo ".Length);
}
literal = literal.ParseVarName(out var name);
var nameString = name.Value();
if (appendTo && scope.PageResult.Args.TryGetValue(nameString, out var oVar)
&& oVar is string existingString)
{
scope.PageResult.Args[nameString] = existingString + strFragment.Value;
return;
}
scope.PageResult.Args[nameString] = strFragment.Value.ToString();
}
}
function
The {{#function}}
block lets you define reusable functions in your page. Unlike other Script Blocks, the body of a function block
is parsed as a code block where only the return value is used. Any output generated from executing the
function is discarded. Use the partial block if you instead want to define reusable fragments.
In its simplest form, defining a function requires the function name and a body that returns a value with the return method, e.g:
This creates a compiled delegate and assigns it to the pages scope where it can be invoked as a normal function, e.g:
hi()
Functions can specify arguments using a JavaScript arguments list:
Where it can be called as normal function or as an extension method:
calc(1,2)
1.calc(2)
Functions can also be called recursively:
{{#function fib(num) }}
#if num <= 1
return(num)
/if
return (fib(num-1) + fib(num-2))
{{/function}}
Although their limited to the configured MaxStackDepth.
Source code for FunctionScriptBlock.cs.
defn
Similar to {{#function}}
above, the {{#defn}}
script block lets you define a function using lisp. The resulting function is
exported as a C# delegate where it can be invoked like any other Script method.
An equivalent calc
and fib
function in lisp looks like:
As in most Lisp expressions, the last expression executed is the implicit return value.
The
defn
Script Block is automatically registered when the Lisp language is registered.
capture
The {{#capture}}
block is similar to the raw block except instead of using its raw text contents, it instead evaluates its contents and captures
the output. It also supports evaluating the contents with scoped arguments where by each property in the object dictionary is added in the
scoped arguments that the block is executed with:
/// <summary>
/// Captures the output and assigns it to the specified variable.
/// Accepts an optional Object Dictionary as scope arguments when evaluating body.
///
/// Usages: {{#capture output}} {{#each args}} - [{{it}}](/path?arg={{it}}) {{/each}} {{/capture}}
/// {{#capture output {nums:[1,2,3]} }} {{#each nums}} {{it}} {{/each}} {{/capture}}
/// {{#capture appendTo output {nums:[1,2,3]} }} {{#each nums}} {{it}} {{/each}} {{/capture}}
/// </summary>
public class CaptureScriptBlock : ScriptBlock
{
public override string Name => "capture";
public override ScriptLanguage Body => ScriptTemplate.Language;
internal struct Tuple
{
internal string name;
internal Dictionary<string, object> scopeArgs;
internal bool appendTo;
internal Tuple(string name, Dictionary<string, object> scopeArgs, bool appendTo)
{
this.name = name;
this.scopeArgs = scopeArgs;
this.appendTo = appendTo;
}
}
public override async Task WriteAsync(
ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
{
var tuple = Parse(scope, block);
var name = tuple.name;
using (var ms = MemoryStreamFactory.GetStream())
{
var useScope = scope.ScopeWith(tuple.scopeArgs, ms);
await WriteBodyAsync(useScope, block, token);
var capturedOutput = ms.ReadToEnd();
if (tuple.appendTo && scope.PageResult.Args.TryGetValue(name, out var oVar)
&& oVar is string existingString)
{
scope.PageResult.Args[name] = existingString + capturedOutput;
return;
}
scope.PageResult.Args[name] = capturedOutput;
}
}
//Extract usages of Span outside of async method
private Tuple Parse(ScriptScopeContext scope, PageBlockFragment block)
{
if (block.Argument.IsNullOrWhiteSpace())
throw new NotSupportedException("'capture' block is missing variable name to assign output to");
var literal = block.Argument.AdvancePastWhitespace();
bool appendTo = false;
if (literal.StartsWith("appendTo "))
{
appendTo = true;
literal = literal.Advance("appendTo ".Length);
}
literal = literal.ParseVarName(out var name);
if (name.IsNullOrEmpty())
throw new NotSupportedException("'capture' block is missing variable name to assign output to");
literal = literal.AdvancePastWhitespace();
var argValue = literal.GetJsExpressionAndEvaluate(scope);
var scopeArgs = argValue as Dictionary<string, object>;
if (argValue != null && scopeArgs == null)
throw new NotSupportedException("Any 'capture' argument must be an Object Dictionary");
return new Tuple(name.ToString(), scopeArgs, appendTo);
}
}
With this we can dynamically generate some markdown, capture its contents and convert the resulting markdown to html using the markdown
Filter transformer:
markdown
The {{#markdown}}
block makes it even easier to embed markdown content directly in web pages which works as you'd expect where content in a
markdown
block is converted into HTML, e.g:
{{#markdown}} ## TODO List - Item 1 - Item 2 - Item 3 {{/markdown}}
Which is now the easiest and preferred way to embed Markdown content in content-rich hybrid web pages like Razor Rockstars content pages, or even this blocks.html WebPage itself which makes extensive use of markdown.
As markdown
block only supports 2 usages its implementation is much simpler than the capture
block above:
/// <summary>
/// Converts markdown contents to HTML using the configured MarkdownConfig.Transformer.
/// If a variable name is specified the HTML output is captured and saved instead.
///
/// Usages: {{#markdown}} ## The Heading {{/markdown}}
/// {{#markdown content}} ## The Heading {{/markdown}} HTML: {{content}}
/// </summary>
public class MarkdownScriptBlock : ScriptBlock
{
public override string Name => "markdown";
public override async Task WriteAsync(
ScriptScopeContext scope, PageBlockFragment block, CancellationToken token)
{
var strFragment = (PageStringFragment)block.Body[0];
if (!block.Argument.IsNullOrWhiteSpace())
{
Capture(scope, block, strFragment);
}
else
{
await scope.OutputStream.WriteAsync(MarkdownConfig.Transform(strFragment.ValueString), token);
}
}
private static void Capture(
ScriptScopeContext scope, PageBlockFragment block, PageStringFragment strFragment)
{
var literal = block.Argument.AdvancePastWhitespace();
literal = literal.ParseVarName(out var name);
var nameString = name.ToString();
scope.PageResult.Args[nameString] = MarkdownConfig.Transform(strFragment.ValueString).ToRawString();
}
}
Use Alternative Markdown Implementation
By default ServiceStack uses an interned implementation of MarkdownDeep
for rendering markdown, you can get ServiceStack to use an alternate
Markdown implementation by overriding MarkdownConfig.Transformer
.
E.g. to use the richer Markdig implementation, install the Markdig NuGet package:
PM> Install-Package Markdig
Then assign a custom IMarkdownTransformer
:
public class MarkdigTransformer : IMarkdownTransformer
{
private Markdig.MarkdownPipeline Pipeline { get; } =
Markdig.MarkdownExtensions.UseAdvancedExtensions(new Markdig.MarkdownPipelineBuilder()).Build();
public string Transform(string markdown) => Markdig.Markdown.ToHtml(markdown, Pipeline);
}
MarkdownConfig.Transformer = new MarkdigTransformer();
keyvalues
The {{#keyvalues}}
block lets you define a key value dictionary in free-text which is useful in Live Documents
for capturing a data structure like expenses in free-text, e.g:
{{#keyvalues monthlyExpenses}} Rent 1000 Internet 50 Mobile 50 Food 400 Misc 200 {{/keyvalues}} {{ monthlyExpenses |> values |> sum |> to => totalExpenses }}
By default it's delimited by the first space ' ', but if the first key column can contain spaces you can specify to use a different delimiter, e.g:
{{#keyvalues monthlyRevenues ':'}} Salary: 4000 App Royalties: 200 {{/keyvalues}}
The KeyValuesScriptBlock.cs
implementation is fairly straight forward where it passes the string body to ParseKeyValueText()
method with an optional delimiter and
assigns the results to the specified variable name:
public class KeyValuesScriptBlock : ScriptBlock
{
public override string Name => "keyvalues";
public override Task WriteAsync(ScriptScopeContext scope, PageBlockFragment block, CancellationToken ct)
{
var literal = block.Argument.Span.ParseVarName(out var name);
var delimiter = " ";
literal = literal.AdvancePastWhitespace();
if (literal.Length > 0)
{
literal = literal.ParseJsToken(out var token);
if (!(token is JsLiteral litToken))
throw new NotSupportedException($"#keyvalues expected delimiter but was {token.DebugToken()}");
delimiter = litToken.Value.ToString();
}
var strFragment = (PageStringFragment)block.Body[0];
var strDict = strFragment.ValueString.Trim().ParseAsKeyValues(delimiter);
scope.PageResult.Args[name.ToString()] = strDict;
return TypeConstants.EmptyTask;
}
}
csv
Similar to keyvalues
, you can specify a multi-column inline data set using the {{#csv}}
block, e.g:
The CsvScriptBlock.cs
implementation is similar to keyvalues
except passes the trimmed string body to FromCsv
into a string List and assigns the result to the specified name:
public class CsvScriptBlock : ScriptBlock
{
public override string Name => "csv";
public override Task WriteAsync(ScriptScopeContext scope, PageBlockFragment block, CancellationToken ct)
{
var literal = block.Argument.ParseVarName(out var name);
var strFragment = (PageStringFragment)block.Body[0];
var trimmedBody = StringBuilderCache.Allocate();
foreach (var line in strFragment.ValueString.ReadLines())
{
trimmedBody.AppendLine(line.Trim());
}
var strList = trimmedBody.ToString().FromCsv<List<List<string>>>();
scope.PageResult.Args[name.ToString()] = strList;
return TypeConstants.EmptyTask;
}
}
partial
The {{#partial}}
block lets you create In Memory partials which is useful when working with partial filters like selectPartial
as
it lets you declare multiple partials within the same page, instead of requiring multiple individual files. See docs on
Inline partials for a Live comparison of using in memory partials.
html
The purpose of the html blocks is to pack a suite of generically useful functionality commonly used when generating html. All html blocks inherit the same functionality with blocks registered for the most popular HTML elements, currently:
script
, style
, link
, meta
, ul
, ol
, li
, div
, p
, form
, input
, select
, option
, textarea
, button
,
table
, tr
, td
, thead
, tbody
, tfoot
, dl
, dt
, dd
, span
, a
, img
, em
, b
, i
, strong
.
Ultimately they reduce boilerplate, e.g. you can generate a menu list with a single block:
{{#ul {each:items, id:'menu', class:'nav'} }} <li>{{it}}</li> {{/ul}}
A more advanced example showcasing many of its different features is contained in the example below:
This example utilizes many of the features in html blocks, namely:
if
- only render the template if truthyeach
- render the template for each item in the collectionwhere
- filter the collectionit
- change the name of each elementit
bindingclass
- special property implementing Vue's special class bindings where an object literal can be used to emit a list of class names for all truthy properties, an array can be used to display a list of class names or you can instead use a string of class names.
All other properties like id
and selected
are treated like HTML attributes where if the property is a boolean like selected
it's only displayed
if its true otherwise all other html attribute's names and values are emitted as normal.
For a better illustration we can implement the same functionality above without using any html blocks:
The same functionality using C# Razor with the latest C# language features enabled can be implemented with:
@{
var persons = (items as IEnumerable<Person>)?.Where(x => x.Age > 27);
}
@if (hasAccess)
{
if (persons?.Any() == true)
{
<ul id="menu-@id" class="nav @(!disclaimerAccepted ? "hide" : "")">
@{
var index = 0;
}
@foreach (var person in persons)
{
<li class="@(index++ % 2 == 1 ? "alt " : "" )@(person.Name == activeName ? "active" : "")">
@person.Name
</li>
}
</ul>
}
else
{
<div>no items</div>
}
}
ServiceStack Blocks
ServiceStack's Blocks are registered by default in #Script Pages that can be registered in a new ScriptContext
by adding the ServiceStackScriptBlocks
plugin:
var context = new ScriptContext {
Plugins = {
new ServiceStackScriptBlocks(),
}
}.Init();
Mix in NUglify
You can configure ServiceStack and #Script
to use Nuglify's Advanced HTML, CSS, JS Minifiers using mix with:
$ mix nuglify
Which will add Configure.Nuglify.cs to your HOST project.
To assist with debugging during development, no minification is applied when DebugMode=true
.
All minifier Blocks supports an additional <name>
argument to store the captured output of the minifier block into, e.g:
{{#minifier capturedMinification}} ... {{/minifier}}
{{capturedMinification}}
That also supports using the appendTo
modifier to concatenate the minified output instead of replacing it, e.g:
{{#minifier appendTo capturedMinification}} ... {{/minifier}}
{{#minifier appendTo capturedMinification}} ... {{/minifier}}
{{capturedMinification}}
minifyjs
Use the minifyjs
block to minify inline JavaScript:
minifycss
Use the minifycss
block to minify inline CSS:
minifyhtml
Use the minifyhtml
block to minify HTML:
svg
Use the svg
block in your _init.html
Startup Script to register SVG Images with ServiceStack.
Removing Blocks
Like everything else in #Script
, all built-in Blocks can be removed. To make it easy to remove groups of related blocks you can just remove the
plugin that registered them using the RemovePlugins()
API, e.g:
var context = new ScriptContext()
.RemovePlugins(x => x is DefaulScripttBlocks) // Remove default blocks
.RemovePlugins(x => x is HtmlScriptBlocks) // Remove all html blocks
.Init();
Or you can use the OnAfterPlugins
callback to remove any individual blocks or filters that were added by any plugin.
E.g. the capture
block can be removed with:
var context = new ScriptContext {
OnAfterPlugins = ctx => ctx.RemoveBlocks(x => x.Name == "capture")
}
.Init();