sebastienros/fluid

Layout support doesn't seem to work in 2.4.0

Closed this issue · 10 comments

We're receiving this error every time we try to parse any template that uses a layout file:

Unknown tag 'layout' at (1:40))

Snippet:

image

(I've also tried it with double quotes, dropping the .liquid extension, etc.)

Interestingly enough, the render tag works just fine in the regular liquid files (non-layouts) even though the fluid-specific documentation does not directly mention it. It only mentions renderbody, rendersection, etc.(?). For now, we are getting by with adding sections to every template. But layouts would be ideal.

The layout tag is only available in the ViewEngine project, are you using it? The default Liquid tags don't have it, this notion of layout and section is extra and opt-in. Try to use FluidViewParser instead of FluidParser.

Ah.. Whoops.
That error went away now that we've switched to FluidViewParser, and now what remains I'm sure is user error with our syntax. It's producing the template without any of the surrounding layout upon render, but we're going to take a few more stabs at it.

Thanks!

Check which file provider is used in the options, like what is the root folder, and if the paths are correct relatively to each other.

@sebastienros I have uploaded this repro to show the issue we are facing. We can't seem to get the examples in the readme working. I'm assuming the physical file provider is set up correctly as the standard render function is working (included in example). Please take a look when you have a chance:

https://github.com/sjd2021/LiquidTemplateTest

Had to look at the tests, but here is how it's done. Note that you are using custom folders so I rooted all the file providers.

// See https://aka.ms/new-console-template for more information

using Fluid;
using Fluid.ViewEngine;
using Microsoft.Extensions.FileProviders;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;

var serializer = JsonSerializer.CreateDefault(new JsonSerializerSettings
{
    ContractResolver = new CamelCasePropertyNamesContractResolver(),
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
    NullValueHandling = NullValueHandling.Include,
    TypeNameHandling = TypeNameHandling.None
});

var options = CreateFluidViewEngineOptions();

var renderer = new FluidViewRenderer(options);

var model = new MyModel(55, "Howdy");

var ctx = new TemplateContext(JObject.FromObject(model, serializer), options.TemplateOptions);

await using var sw = new StringWriter();
await renderer.RenderViewAsync(sw, "raw.liquid", ctx);

Console.WriteLine(sw.ToString());

static FluidViewEngineOptions CreateFluidViewEngineOptions()
{
    var options = new FluidViewEngineOptions();
    options.ViewsFileProvider = new PhysicalFileProvider(AppContext.BaseDirectory);
    options.PartialsFileProvider = new PhysicalFileProvider(AppContext.BaseDirectory);
    options.TemplateOptions.ValueConverters.Add(value => value is JValue { Type: JTokenType.Boolean } v ? (bool?)v.Value : null);
    options.TemplateOptions.MemberAccessStrategy.IgnoreCasing = true;
    options.TemplateOptions.FileProvider = new PhysicalFileProvider(AppContext.BaseDirectory);
    return options;
}

record MyModel(int PropertyA, string PropertyB);

Also you are using a Json document, which would be unnecessary here, you can pass a MyModel instance directly. And maybe use System.Text.Json instead of NewtonSoft, that would make one less dependency.

@sebastienros Ahh that does work. Thank you!

So, for the sample, I used a static file.. But in reality, we don't have one set document store and we actually pass in these small snippets of html that we then use to cache the fluid template based on an autogenerated key. These can come from the database, a cloud storage location, in-memory, etc., but the layouts are static since they control the styles and footers which will never change for our company on an individual basis.

Is there a simple way to make this work with html strings instead? We do cache the templates based on an auto-generated key so I don't think it's super inefficient or anything.

Btw the reason I have gotten into the habit of using the jobject method is because my real use case has too many types to explicitly list, and we only pass known models into the service that handles these calls. It also allowed us to replace tokens (overrides) at a customer level pretty easily using selecttoken, etc.

You can use in-memory file providers. The tests use one for instance. And then you can use CompositeFileProvider if you need to have snippets come from different sources at the same time.

Got it. Will take a look

Update: it's working great. I just needed to use the NullEncoder to override the DefaultHtmlEncoder in order to finish converting everything over to this.

That's exactly how it's working, all good.
There is a special filter otherwise like @Html.Raw which is {{ value | raw }} so the output is not encoded if you want to keep the default html encoder for security reasons. There is also a tag: {% raw %} ... {% endraw %}. This is explained in the README.