Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Looping (webc:for) #28

Closed
darthmall opened this issue Sep 30, 2022 · 26 comments
Closed

Looping (webc:for) #28

darthmall opened this issue Sep 30, 2022 · 26 comments
Labels
enhancement New feature or request
Milestone

Comments

@darthmall
Copy link

darthmall commented Sep 30, 2022

Flow control — conditionals and loops — is handled with render functions. This is powerful, since you can write arbitrary JavaScript, but it’s a bit ugly in the template. Even a simple loop listing posts from an Eleventy collection is a bit noisy, and it’s not necessarily obvious what is happening inside the <script> tag.

Although you could combine WebC with another template language (e.g. Nunjucks) for flow control, it would be nice to have a less verbose way to write loops and conditionals, similar to how we have @html for setting the contents of an element.

Example loop

For context, this is a simple loop generating a list of links to pages from an Eleventy collection using a render function.

<ul>
  <script webc:type="render" webc:is="template">
    function () {
      return this.collections.post.map((p) => `<li>
        <a href="${p.url}">${p.data.title}</a>
      </li>`);
    }
  </script>
</ul>

Update: As of WebC 0.7, the above render function can now be written using webc:type=js like this, which is a little more succinct.

<ul>
  <script webc:type="js">
    this.collections.post.map((p) => `<li>
      <a href="${p.url}">${p.data.title}</a>
    </li>`);
  </script>
</ul>

Inspiration

One solution to this problem could look like how Vue handles this with an attribute; v-for in Vue, so maybe @for in WebC.

<ul>
  <li @for="p in this.collections.post">
    <a :href="p.url" @html="p.title"></a>
  </li>
</ul>

Another option might be the JSX-like approach in Astro. Perhaps instead of using {} as if it’s a template literal the way Astro does, the render function script tag could just omit the function and return statement and just return the value of the last statement (like Ruby).

<ul>
  <script webc:type="render">
    this.collections.post.map((p) => `<li>
      <a href="${p.url}">${p.data.title}</a>
    </li>`
  </script>
</ul>
@zachleat zachleat added the enhancement New feature or request label Oct 1, 2022
@MWDelaney
Copy link

MWDelaney commented Oct 1, 2022

Okay, I don't have a whole thought here but I want to get it down:

Web components use <template> for reusable markup, but using those for loops and conditionals ships more JavaScript for something that ought to be pre-rendered.

Webc uses the @ and : stying, which I haven't dug in hard enough to know why which is used when, so please forgive me if I get these backwards, or any of this markup wrong. I'm also wildly guessing at how the data would look.

For loops, what about something like

<ul :each="`data`" :as="`item`">
  <@template>
    <li @html="`item.name`"></li>
  </@template>
</ul>

Conditionals are harder because you want to be able to use them inline with other markup, which would make them harder to read rather than easier to read.

Conditionals are kinda when you cross over from being markup to being something else.

@toddmorey
Copy link

toddmorey commented Oct 7, 2022

Conditionals are kinda when you cross over from being markup to being something else

Definitely agree with that. What I've found from working on Vue codebases is that it can be easy to overlook a control structure when it's expressed as an attribute on an HTML node. Compare:

<li @for="p in this.collections.post">{post.title}</li>

with the Svelte's handlebars-inspired approach:

{#each this.collections.post as post}
   <li>{post.title}</li>
{/each}

It just feels breaking the control structures out to be their own entities feels more honest, explicit, and readable.

@toddmorey
Copy link

Another incomplete thought.... (Blame @MWDelaney for setting the precedent!)

Could you implement syntactic sugar for flow control as... just a small library of web components? Would you want to?

<for-each :in="`data`" :as="`item`">
   ...
</for-each>

Not sure if that's doable or advisable.

@MWDelaney
Copy link

MWDelaney commented Oct 7, 2022

Oh heck are we learning why WordPress went with HTML comments in real time?

<!-- webc:each data as item -->
  <li @html="`item.title`"></li>
<!-- webc:endeach -->

@MWDelaney
Copy link

Another incomplete thought.... (Blame @MWDelaney for setting the precedent!)

Could you implement syntactic sugar for flow control as... just a small library of web components? Would you want to?

<for-each :in="`data`" :as="`item`">

   ...

</for-each>

Not sure if that's doable or advisable.

Could you ship this separately and then build special-class support for it into webc to pre-render it?

@imacrayon
Copy link

I'd suggest looking to Alpine's directive syntax for inspiration here. It's inspired by Vue but uses the real DOM so it's more HTML-forward. For example the x-for and x-if attributes only work on <template>. A WebC parallel might look like this:

<ul>
    <template @for="post in this.collection.posts">
        <li><a :href="post.url" @html="post.data.title"></a></li>
    </template>
</ul>
<template @if="this.title">
    <h1 @html="this.title"></h1>
</template>

@MWDelaney
Copy link

MWDelaney commented Oct 9, 2022

I'd suggest looking to Alpine's directive syntax for inspiration here. It's inspired by Vue but uses the real DOM so it's more HTML-forward. For example the x-for and x-if attributes only work on <template>. A WebC parallel might look like this:


<ul>

    <template @for="post in this.collection.posts">

        <li><a :href="post.url" @html="post.data.title"></a></li>

    </template>

</ul>


<template @if="this.title">

    <h1 @html="this.title"></h1>

</template>

This is genuinely the whole version of the half-thought I had above. Thank you for this.

I mean this in a credit-giving, not a credit-taking way.

@zachleat
Copy link
Member

Just for folks visiting this issue prior to its completion (and do not take this comment as detracting from the enhancement proposed here) this is possible today using webc:type (using Eleventy template syntax or JS syntax)

@darthmall
Copy link
Author

Right. It was actually putting together the home page for https://11ty.webc.fun/ that prompted me to open this issue in the first place. I use a render function to list recent posts there. An example of looping with render functions is on the site, if anyone wants to see how I did it.

@marisademeglio
Copy link

This plus reprocessing would be beautiful together!

@WickyNilliams
Copy link

WickyNilliams commented Oct 15, 2022

For fun i experimented with putting jsx directly in a template tag using a custom transform:

<template webc:type="jsx">
  <my-component>
    <p>Hello, {this.name}!</p>
    <ul>
      {[1, 2, 3].map(i => <li>{i}</li>)}
    </ul>
    <p>
      <slot></slot>
    </p>
    {Math.random() > 0.5 ? <span>high</span> : <span>low</span>} 
  </my-component>
</template>

It works sort of like the built in render type, but first it passes the template contents through esbuild to transform the jsx

Edit: in case it's not clear, this results in a plain html string, no virtual DOM or anything like that

@dwkns
Copy link

dwkns commented Oct 24, 2022

One thing that I think would really benefit beginners would be differentiating variables/code from strings.

Take this line for instance:

<li @html="title"></li>

It is not clear to a beginner if title is a variable or a string. Does it always have to be a variable? Or can it be a string? How do you differentiate?

Consider instead this:

<li @html={ title }></li>

It's much clearer that @html={ title } is referring to a variable. And if you need a string instead you could do: @html={ "title" } or even @html={ The title is: ${ title }}

To my mind this is much simpler to understand.

Also this:

<li @html={ title }></li>

Could then be syntactic sugared into

<li>{ title }</li>

Having the content between the HTML tags makes it a little clearer.

I suspect that this would also make creating a styling/formatter plugin for VSCode that much easier to accomplish.

@zachleat
Copy link
Member

zachleat commented Nov 16, 2022

Follow along to #73 for render functions that just return the value from the last statement (similar to dynamic attributes).

Lots of amazing inspiration in this thread!

Gotta say my favorite so far is: #28 (comment)

Though I think it’d be more intuitive to copy JS looping syntax (with in and of) here:

<ul>
    <template webc:for="let entry of myArray">
        <li @html="entry"></li>
    </template>
</ul>
<ul>
    <template webc:for="let key in myObject">
        <li @html="myObject[key]"></li>
    </template>
</ul>

zachleat added a commit that referenced this issue Nov 17, 2022
zachleat added a commit that referenced this issue Nov 17, 2022
@zachleat zachleat mentioned this issue Nov 17, 2022
@zachleat
Copy link
Member

Moved webc:if to its own issue at #76

@zachleat
Copy link
Member

I still think there is a place for webc:for but I’m going to sit on it for a bit more and see if WebC v0.7.1 features webc:if and webc:type="js" are good enough for now.

@MWDelaney
Copy link

webc:for would be a huge boon when replacing other languages like njk and liquid. Which I understand is not entirely WebC's goal.

What if the Eleventy plugin added for?

@zachleat zachleat changed the title Syntactic sugar for flow control Syntactic sugar for flow control (webc:for) Feb 13, 2023
@solution-loisir
Copy link

solution-loisir commented Feb 21, 2023

I experimented a bit with eleventy-plugin-webc. With the use of a filter function say:

config.addFilter("loop", function(array, callback) {
  return array.map((item, index, source) => callback(item, index, source)).join("\n");
});

Now I can write something like:

<ul @html="loop(collections.all, (item) => `
  <li>${item.data.title}</li>
  <ul>
    <li>${item.data.description}</li>
    <li><a href='${item.page.url}'>${item.page.fileSlug}</a></li>
  </ul>`
)">
</ul>

I can imagine an integration where you could have:

<ul @loop="(collections.all, (item) => `
  <li>${item.data.title}</li>
  <ul>
    <li>${item.data.description}</li>
    <li><a href='${item.page.url}'>${item.page.fileSlug}</a></li>
  </ul>`
)">
</ul>

Just an idea. 😄

@d3v1an7
Copy link

d3v1an7 commented Mar 9, 2023

Have used alpine before and am a big fan :) I tried some of the solutions above, but ended up falling back to njk when looping, for now.

My halfway solution for now is:
template.webc

<my-component-njk :data="objectWithData"></my-component-njk>

component/my-component-njk.webc

<template webc:type="11ty" 11ty:type="njk">
  <ul>
  {% for item in data %}
    {% if item.title %}<li>{{item.title}}</li>{% endif %}
  {% endfor %}
  </ul>
</template>

@mirisuzanne
Copy link

As far as I can tell, this wouldn't just be syntax sugar. Maybe I'm missing something? Currently, using webc:type="js" I can output simple strings that will be parsed as webc components, but there's no way to pass in more complex properties to those components - like objects or arrays. Everything is flattened to a string. That's extremely limiting.

Maybe I could get clever and use item indexing to generate more complex arguments that get output in dynamic attributes and re-compiled? But that feels like a hack, if it would even work?

The alternative is mixing template languages, which gets messy fast. Especially since the render shortcodes expect an object, and liquid has no way of creating objects on the fly…

@zachleat zachleat pinned this issue Mar 15, 2023
@okmanideep
Copy link

Agree with @mirisuzanne here. It's not appropriate to call this "sugar". This enables common use cases that are currently not possible to do in webc.

When using 11ty, we can use liquid or other template languages and use renderTemplate with webc like this

<main role="main">
{%- for item in collections.all reversed %}
	{% renderTemplate "webc", item.data %}
		<post-item webc:nokeep></post-item>
	{% endrenderTemplate %}
{% endfor -%}
</main>

This is 11ty providing a way to do this. Not necessarily webc. AFAIK, there is no way to loop over an array and pass objects down the hierarchy with webc.

@zachleat zachleat changed the title Syntactic sugar for flow control (webc:for) Looping (webc:for) Mar 22, 2023
@zachleat zachleat added this to the WebC v0.9.4 milestone Mar 22, 2023
@zachleat
Copy link
Member

webc:for will ship with WebC v0.9.4!

Docs are up at https://www.11ty.dev/docs/languages/webc/#webcfor-loops

@zachleat
Copy link
Member

Implementation feedback is welcome—looking to ship WebC v0.9.4 somewhat soon (within the next few business days)

@sombriks
Copy link
Contributor

This is awesome and opens new possibilites!
for the record, i was iterating using render functions: https://github.com/sombriks/koa-webc-examples/blob/main/004-using-with-other-middlewares/views/result.webc

now i'll make another example using webc:for so i can showcase more ways to do the same!

thank you for your amazing work!

@d3v1an7
Copy link

d3v1an7 commented Mar 23, 2023

Wow @zachleat, this is extremely rad. Thank you! Just doing a quick test (grabbed 11ty repo and dropped it into my project, overriding the existing folder at node_modules/@11ty/webc -- let me know if there is a better way to do this before tags exist!)

This works as expected
src/index.webc

<card-story webc:for="item of arrayOfPosts" :title="item.title" :excerpt="item.excerpt"></card-story>

src/_components/card-story.webc

<article>
  <h3 @text="title"></h3>
  <p @text="excerpt"></p>
</article>

As does this!
src/index.webc

<card-story-alt webc:for="itemAlt of arrayOfPosts" :@data="itemAlt"></card-story-alt>

src/_components/card-story-alt.webc

<article>
  <h3 @text="data.title"></h3>
  <p @text="data.excerpt"></p>
</article>

Am very excited about this :)

⚠️ I've noticed some weirdness on follow up builds using npx @11ty/eleventy --serve, where the data object seems to get 'lost', which errors the build like:

[11ty] Problem writing Eleventy templates: (more in DEBUG output)
[11ty] 1. Having trouble rendering webc template ./src/index.webc (via TemplateContentRenderError)
[11ty] 2. Evaluating a dynamic attribute failed: `:title="item.title"`.
[11ty] Original error message: Cannot read properties of undefined (reading 'title') (via Error)

Stopping the server and starting it again sorts it out, but only for the first build. I'm doing tests in an existing project, so will set up a cleaner env to see if I can pin it down.

arrayOfPosts is being populated via a index.11tydata.js, and looks something like

module.exports = {
  eleventyComputed: {
    arrayOfPosts: async data => {
      return await {api call}

Edit 1: Here is a quick test repo , inspired by @mdarrik in #115

Edit 2: Fetching of dynamic data doesn't seem to be related. I've got a branch here with a hardcoded file in _data which also demonstrates the same behaviour.

@zachleat
Copy link
Member

@d3v1an7 can you copy and paste your comment whole-sale to a new issue? (please!)

@zachleat zachleat unpinned this issue Mar 24, 2023
@zachleat
Copy link
Member

Just FYI this milestone changed to 0.10.0!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests