Checking for Values: True and Falsey

A lot of template development involves checking that something “exists.” This question is weirdly deep and existential, so we’re going to start off as simple as possible, then dig deeper.

  • When checking strings, compare against blank
  • When checking collections, compare against empty
  • When checking anything else, use standard comparison operators

Why does it get any weirder than that? Because “exists” could mean a lot different things, and even more when you don’t know what the value type you’re comparing is. To say something “exists” could mean:

  • That a value of any kind is in the Context
  • That the value is not null
  • If it’s a boolean, that the value is true
  • That text is not empty or whitespace
  • That a number is above 0
  • That a collection has at least one element
  • That a date is not the default date (DateTime.MinValue)

To dig a little deeper, we’ll discuss how the conditionals actually perform, then we’ll dig into the theory behind it and how you can change it if you like. Fluid gives your some wonderful customization options to make conditional logic as easy as possible to your template developers.

Let’s start by assuming name is text (a StringValue).

Just doing this will check if it is in the Context.

{% if name %} This is a name! {% endif %}

That doesn’t test if name has a value, but just that it exists in the Context.

You could also check the size property of name.

{% if name.size > 0 %} This is a name! {% endif %}

That would verify that (1) is exists in the Context, and (2) it’s not empty (however, it could be whitespace).

To make this easier, there is a “magic string” that corresponds to a special FluidValue. The string is blank, and the parser replaces it with an instance of BlankValue, which is a FluidValue (more on this below).

blank rolls up a bunch of checks into one. For example:

{% if name != blank %} This is a name! {% endif %}

That will ensure that name (1) exists in the Context, (2) is not empty, and (3) is not whitespace. For strings, this should be your go-to conditional.

For collections, there is a magic string called empty, which is an instance of EmptyValue.

{% if children != empty %} There are children! {% endif %}

This will verify that children both (1) exists in the context, (2) has at least one item.

(Note that if you’re looping, there is an else block that can be used inside the for block to cover situations when the collection is empty. This is probably easier than a conditional. See Looping.)

Here’s what’s happening under the hood –

Whenever you perform a conditional in Fluid, you’re comparing two FluidValue objects.

So, if we do this:

{% if name == blank %}

What we’re doing is comparing a (probably) StringValue to a BlankValue, both of which are FluidValue and overload Equals for comparisons. This comparison causes the Equals method on the left value to be called, and the right value is passed to it.

In C# terms, it looks like this:

var name = new StringValue("whatever");
var blank = new BlankValue();
if(name.Equals(blank)) {
    ...
}

Every FluidValue has to override Equals, which is where it does its logic.

public override bool Equals(FluidValue other)
{
  // Comparison logic happens here
}

We can make our own value to use for comparisons, if we want. We just need to extend from FluidValue.

For example, we want to check if something is the number 42 or the string “forty two”. (See this article for why. Or don’t bother.)

public class SecretOfLifeTheUniverseAndEverything : FluidValue
{
  // This covers comparisons in both directions
  public override decimal ToNumberValue() => 42M;
  public override string ToStringValue() => "forty two";
  public override bool Equals(FluidValue other)
  {
    if (other.Type == FluidValues.Number && other.ToNumberValue() == ToNumberValue()) return true;
    if (other.Type == FluidValues.String && other.ToStringValue().ToLower() == ToStringValue()) return true;
    return false;
  }

  // We don't really care about any of this, but FluidValue is
  // abstract, so we have to override it all
  public override void WriteTo(TextWriter w, TextEncoder e, CultureInfo c) => w.Write(string.Empty);
  public override FluidValues Type => FluidValues.String;
  public override bool ToBooleanValue() => false;
  public override object ToObjectValue() => ToStringValue();
}

We’ve created a FluidValue that exists for no other reason than comparisons. We can simply put this in a context:

context.SetValue("the_secret_of_life", new SecretOfLifeTheUniverseAndEverything());

(Note: this is how empty and blank used to work – they were just placed in the global Context Scope in the TemplateOptions constructor. Later, they were moved to be parser constructs.)

Now we can compare against it.

{% assign answer = 42 %}
{% if answer == the_secret_of_life %}
	This is the secret!
{% endif %}

{% assign another_answer = "forty two" %}
{% if another_answer == the_secret_of_life %}
	This is the secret!
{% endif %}

When using the == operator, the FluidValue on the left is in control.

The left side will do the comparison using its Equals method. This is different for every FluidType, but in general the Type property of the right value will be checked, and methods like ToNumberValue() and ToStringValue() will be called on the right value to do the comparison.

You may have edge cases with custom values where its better to check from the other direction – you have such weird logic that you can’t “trust” any of the default FluidValue objects to get it right. You want your Equals method to be used in all cases.

{% if the_secret_of_life == answer %}

This is clearly not ideal, as conditionals should work from both directions. But if you have to do this, make sure your template developers understand the logic and the reasoning.

Another option here is to write a custom operator that switches the logic. Here’s an example of an operator we’ll call is:

public class IsBinaryExpression : BinaryExpression
{
  public IsBinaryExpression(Expression left, Expression right) : base(left, right) {} 

  public override async ValueTask<FluidValue> EvaluateAsync(TemplateContext context)
  {
    var leftValue = await Left.EvaluateAsync(context);
    var rightValue = await Right.EvaluateAsync(context);

    return BooleanValue.Create(rightValue.Equals(leftValue));
  }
}

The key is in the last line – we’re simply using the Equals methods from the right side value, rather than the left.

We can register our operator like this –

parser.RegisteredOperators["is"] = (a, b) => new IsBinaryExpression(a, b);

And now we can use it like this:

{% if answer is the_secret_of_life %}