11ty / eleventy

A simpler site generator. Transforms a directory of templates (of varying types) into HTML.
https://www.11ty.dev/
MIT License
17.09k stars 494 forks source link

Don’t convert computed (Liquid) variables to strings #1069

Closed paulrobertlloyd closed 4 years ago

paulrobertlloyd commented 4 years ago

Is your feature request related to a problem? Please describe. I have a dataset of addresses, and some of its values are objects. For example:

{
  title: 'Connaught Theatre',
  address: {
    'street-address': 'Union Place',
    locality: 'Worthing',
    region: 'Sussex',
    'country-name': 'United Kingdom',
    'postal-code': 'BN11 1LG',
    'plus-code': '9C2XRJ7H+FR'
  },
  geo: {
    latitude: 50.813688,
    longitude: -0.370437,
    resolution: 0.000125
  }
}

Generating pages from data, I paginate over these values, aliasing the paginated item to page.

So that I can use the same variables across shared templates and includes – both for pages generated from both files and data – I use eleventyComputed to reassign these values to shorthand variables:

---yaml
layout: venue
pagination:
  data: venues
  size: 1
  alias: page
  addAllPagesToCollections: true
permalink: "events/venues/{{ page.fileSlug }}/"
vocab: card
changefreq: monthly
priority: 0.2
eleventyComputed:
  title: "{{ page.data.title }}"
  address: "{{ page.data.address }}"
  geo: "{{ page.data.geo }}"
---

However, it appears that Eleventy converts computed variables to strings. If I inspect both the ‘raw’ and computed template variables (using LiquidJS’s built-in json filter):

page.data.title: {{ page.data.title | json }}
title: {{ title | json }}

page.data.address: {{ page.data.address | json }}
address: {{ address }}

page.data.geo: {{ page.data.geo | json }}
geo: {{ geo }}

[object Object] is output:

page.data.title: "Connaught Theatre"
title: "Connaught Theatre"

page.data.address: {"street-address":"Union Place","locality":"Worthing","region":"Sussex","country-name":"United Kingdom","postal-code":"BN11 1LG","plus-code":"9C2XRJ7H+FR"}
address: [object Object]

page.data.geo: {"latitude":50.813688,"longitude":-0.370437,"resolution":0.000125}
geo: [object Object]

Describe the solution you'd like Not sure if there are any side effects to this, but a (Liquid) variable in eleventyComputed should have the same output as a (Liquid) variable in a template.

BONUS REQUEST: It’d sure be nice if templates could use ‘longhand’ variables as well as ‘shorthand’ ones, i.e. {{ page.data.title }} == {{ title }} (this would also bring parity with the equivalent variables used in collection loops). That way, I wouldn’t need to create these computed shorthand variables in the first place!

paulrobertlloyd commented 4 years ago

In addition, this of course also means values that are false will return empty strings:

page.data.foo: {{ page.data.foo | json }}
foo: {{ foo | json }}

gives:

page.data.foo:
foo: ""
paulrobertlloyd commented 4 years ago

@zachleat My apologies. I had a very insightful thought in the shower this morning; the bugs I’m finding in 11ty all relate to Liquid, and I’m using my own LiquidJS instance (v9.x). Sure enough, downgrading Liquid to use the built in version means the above issue no longer presents itself.

Linking this to PR #1058 as something to check when this upgrade is merged in.

paulrobertlloyd commented 4 years ago

Hmmm, actually no, this is still a problem, regardless of Liquid version. 😔

I have a url value in my dataset, that returns either a string value, or undefined. Assigning it’s value using eleventyComputed, I get a different result when I test for its truthiness. For example, using the template:

page.data.url returns {% if page.data.url %}truthy{% else %}falsy{% endif %}
url returns {% if url %}truthy{% else %}falsy{% endif %}

the following is output:

page.data.url returns falsy
url returns truthy

Here’s an even simpler example. With the given frontmatter:

foo: false
eleventyComputed:
  foo: "{{ foo }}"

and the following template:

foo returns {% if foo %}truthy{% else %}falsy{% endif %}

the following is output:

foo returns truthy

So even though foo has been set to be false, once it’s computed, it becomes something else equating to a truthy value.

paulrobertlloyd commented 4 years ago

And there’s completely different behaviour regarding objects when using computed data and Liquid v6. In this case, it is possible to access computed object values.

So, with a geo object that contains longitude and latitude values, and the following template:

<pre>
page.data.geo: {{ page.data.geo }}
geo: {{ geo }}

page.data.geo.longitude: {{ page.data.geo.longitude }}
geo.longitude: {{ geo.longitude }}
</pre>

the following is output:

page.data.geo: [object Object]
geo: [object Object]

page.data.geo.longitude: -0.370437
geo.longitude: 

Tl;DR: Computed data != original data, regardless of Liquid version. Phew! My head’s spinning… 🤪

zachleat commented 4 years ago

Ah sorry after a bead I think there are a few hefty confusing limitations here that need a little bit more love.

Considering this a blocker for 0.11

zachleat commented 4 years ago

Did a somewhat hefty refactor of computed data for #973 but I haven’t had a chance to retest this issue specifically yet. If you get a chance can you use the latest GitHub and see if this is fixed for you? Don’t dig too deep if it’s not—I’ll circle back here before 0.11.0 ships

paulrobertlloyd commented 4 years ago

Tried testing this but ran into a new issue #1098 🤦🏼‍♂️

zachleat commented 4 years ago

Another push has been pushed!

paulrobertlloyd commented 4 years ago

Now that the issue in #1098 has been fixed, I tried this again, but still appear to be getting the same result, sadly.

zachleat commented 4 years ago

@paulrobertlloyd I would expect those template strings to operate how they are currently operating, I think. There are two forks right now to computed data: 1. Template strings (renders through a template engine) and 2. everything else (usually a JS function though)

In Liquid JS, rendering a variable that has false as the value will still render a truthy string.

For example, here’s a test I just wrote that passes (has nothing to do with computed data):

test("Liquid Render a false #1069", async t => {
  let fn = await new TemplateRender("liquid").getCompiledTemplate(
    "{{ falseValue }}"
  );
  t.is(await fn({ falseValue: false }), "false");
});

Is that unexpected behavior to you? Either way this merits a warning in the docs.

TLDR: strings beget strings. If you want different primitives you need to use function computed data.

zachleat commented 4 years ago

I’m almost tempted to remove the template string method altogether, but it is the only way to use computedData in YAML front matter right now which is the price we’re paying here.

paulrobertlloyd commented 4 years ago

Thanks for looking at this, unfortunate to hear that it’s a limitation. However, the more I think about this, the more I think this is a symptom, not the cause.

This issue stems from addressing data which can come from a variety of different sources, be it global/directory data, collections, pagination, or frontmatter. It’s all a bit confusing, and throwing computed data into the mix is probably only making things more complicated, at least from my perspective.

Also, turns out I was also doing something really stupid. I didn’t think to add the pages generated by pagination to their own collection, and then query that collection directly, but instead tried to model page data in my global data files. I’ve stopped doing that now, and as a result have a better understanding of data is modelled across these different sources and methods.

zachleat commented 4 years ago

Just do keep in mind that this limitation is template language specific and only in play when you use raw strings. If you use a JS function in your computed data you can return any arbitrary object.

Going to close this for now—let me know if you have more questions!

paulrobertlloyd commented 4 years ago

Nope, all good on this front! 😃

anantakrishna commented 1 year ago

TLDR: strings beget strings. If you want different primitives you need to use function computed data.

I believe it's necessary to reflect this in the documentation. As of now it's unclear that we can get only string values in the front matter.