svelte: {#range ...} block

Never thought I’d say this but I think we need range blocks — we’ve had so many questions along the lines of ‘how do I iterate n times?’.

The usual answer is one of these…

{#each Array(n) as _, i}
  <p>{i}</p>
{/each}
{#each { length: n } as _, i}
  <p>{i}</p>
{/each}

…but neither is particularly satisfying.

Anyway, we’re a compiler, so we can add this for free, if we want to. The only real question is syntax. We could emulate Ruby’s range operator:

<!-- 1,2,3,4,5 -->
{#range 1..5 as n}
  {n}
{/range}

<!-- 1,2,3,4 -->
{#range 1...5 as n}
  {n}
{/range}

{#range 5 as n} could be shorthand for {#range 0...5 as n}, perhaps.

Complications: Ruby’s operator also handles decrementing ranges (5...1) and strings ('a'...'z' and 'z'...'a'), so if we were to steal that syntax then presumably we should also support those.

Any thoughts?

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 119
  • Comments: 51 (16 by maintainers)

Most upvoted comments

Happily, now Svelte 4 has added support for doing {#each} over iterables, so doing this has become possible, and therefore we probably don’t really need a new syntax for this.

Just let us omit the as variable part and we’re good to go, the syntax would be as concise as:

{#each range(1, 5)}

{/each}

I’m honestly all in for a range block. I dislike Ruby’s syntax, though. 😕

Nim, swift and others use < to denote open intervals:

{#range 1..5 as n}  // closed: 1,2,3,4,5
{#range 1..<5 as n} //   open: 1,2,3,4

It’s not exactly pretty, but it’s much more intuitive than Ruby’s, imo. Maybe there’s something else entirely?

On your last point, I think even with variables (ie. runtime juggling), decrementing ranges would be fairly minimal to support. I don’t know about characters…

If you don’t want to add a new syntax for this, I think the most reasonable route to take would be for {#each} to start supporting iterables (this issue), so that we can use a generator range function like so:

export function* range(start: number, end: number, step: number = 1) {
    for (let i = start; i <= end; i += step) {
        yield i;
    }
}

Usage:

{#each range(1, 5) as num}

{/each}

Nice and simple. Currently you’d have to do {#each [...range(1, ,5)] as num} which is a bummer. Additionally, it would also be neat if the as [var] part was made entirely optional; so that if you don’t need the current number, you could just omit that part and simply do:

{#each range(1, 5)}

{/each}

Currently this isn’t possible and the Svelte compiler will throw an error if you don’t have the as [var] part. It’s been 2 years now since this issue was created and it’s the fifth most upvoted open issue in the repo, I think it deserves a bit more love.

Agree with @onkel-dirtus to use #each block.

{#each 5 as n}
  {n}
{/each}

{#each 1..5 as n}
  {n}
{/each}

I’m actually the opposite, I really like the ruby range syntax. I think it is simple and clear.

so this is not moving b/c we can’t coalesce around a syntax - IMHO they are all pretty much the same.

How about through and to? Similar to Sass @for loops. If to is used, the final number is excluded; if through is used, it’s included:

{#range 1 through 5 as i}
    <!-- Do stuff... -->
{/range}

and

{#range 1 to 5 as i}
    <!-- Do stuff... -->
{/range}

Optionally, the first number + the to/through keyword could also be omitted, they are 0 and through by default, respectively:

{#range 5 as i} <!-- Same as "range 0 through 5 as i" -->
    <!-- Do stuff... -->
{/range}

Perhaps optionally with an additional step clause:

{#range 1 through 10 step 2 as i}
{/range}

There is a tc39 proposal to add a range function to JavaScript. This would allow you to iterate over a range in svelte using {#each Number.range(1, 3) as i}. It may not be worth adding a new operator to svelte if there is a upcoming change to JavaScript that would effectively solve this problem.

edit: actually the syntax would have to be {#each [...Number.range(1, 3)] as i} which is slightly less ergonomic (unless Svelte starts supporting looping over iterables)

I’m putting this in as a comment (instead of an issue). I’m proposing a simple, concise repeat tag. This is in addition to {#each} (and other proposed variants.)

eg.

{#repeat 10}

{/repeat}

would repeat the code block 10 times from 1 to 10 inclusive.

{#repeat 10 i}

{/repeat}

same but puts the value in variable “i”

let n = 5;
{#repeat n i}

{/repeat}

would repeat the code block “n” times from 1 to n inclusive.

CURRENT APPROACH:

{#each [1,2,3,4,5,6,7,8,9,10] as i}
{/each}

but if this was 100, it requires more code:

let array = [...Array(100).keys()];
{#each array as i}

{/each}

BENEFITS:

  • Concise
  • Easy to read
  • Common operation

NOTES:

In teaching Svelte to children, this would make a big difference. In fact, teaching to children is usually a good litmus test as to whether something is easy to use and understand. The repeat tag is not a substitute for other looping tags like “each”.

{#each 5 as n} is bad because we won’t be able to statically determine whether this is a range or an array iteration. (Unless it only works with literal numbers, which is also bad.) Whatever the syntax is, it needs to be clear at compile time whether we are doing a range thing or an array thing.

If this feature will not impact the generated code on projects that doesn’t use it, there is no impediment, IMHO.

Regarding stepping/incrementing, an additional, very concise syntax would be this:

0..10..100  // 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100

(E.g. used by F#)

This seems to be the place where discussions of each over iterables ends up. Is it really right that each doesn’t, and possibly won’t, natively support iterables? Surely they’re canonically what you iterate over? (Admission: I’m coming from C#, where that’s certainly true, with a different name, i.e. for Enumerable/IEnumerable/IEnumerable<T>.)

In #894 the reasons for refusing to allow each over object properties were presented together with the reasons for refusing to iterate over iterables. I’m not arguing for each over object properties, because I’d say far fewer people would be surprised by having to do each Object.entries(obj) than apparently are surprised (#4408) by having to do each [...iterable] (or in real life, e.g., each [...seq.filter(f).take(5)] instead of each seq.filter(f).take(5)), simply because… the iterable is already iterable!

I think that is why conduitry said:

Unless it only works with literal numbers, which is also bad.

I am in agreement here, limiting it to only literal numbers reduces the usefulness of range syntax. The point, to me, is to provide a more elegant syntax for iterating a set number of times without requiring an array-like. Forcing people who are using values only known at runtime to go back to { length: n } defeats the point somewhat.

I do not have much experience in open source community (yet), but I found this community very diverse and open, hence daring to share my opinion.

I am sorry, but I am bit skeptical about this. I believe learning new language with minimal syntax is much easier and possibly less confusing. So, feeling uncomfortable having a separate block for special case of iterating.

Also, afraid that there is a possibility of #each and #range blocks getting used interchangeably. i.e. #range on arrays and #each on numbers, leading to more queries and clarifications.

I don’t think I am capable enough to suggest, but it would be good if there can be syntax addition for range in #each block itself.

#each came from the popularity (at that time) of handlebars afaik, but if we stop and review the current syntax/workarounds req to implement each of the previous 3/4 looping requirements then maybe @Rich-Harris was right earlier.

Surely much more natural to js developers and gives a elegant way to implement asc/desc ranges with optional steps too.

Maybe time to bite the bullet and deprecate #each and replace it with #for? Would mean no more #each {length: n} kludge either : )

We managed to get rid of handlebars double curlies {{ and }} and replaced them with single { } brackets so seems like a good time to say goodbye to handlebars #each finally.

https://github.com/sveltejs/svelte/issues/894 was interesting on this. From @Rich-Harris himself:

Maybe we should have a for:

{{#for key in object}}
  {{object[key]}}
{{/for}}

{{#for thing of iterable}}
  {{thing}}
{{/for}}

{{#for [key, value] of map}}
  {{key}}: {{value}}
{{/for}}

These didn’t make the cut but were very clean indeed (superior to what we have currently?). For ranges we could use:

{#for 1 to 10 as n}
  {n}
{/for}

// optionally have a step clause
{#for 1 to 10 step 2 as n}
  {n}
{/for}

It can’t be statically analysed if a variable is used instead of a literal number (n instead of 5), which means the range would only work for literal numbers and not variables, in this case.

The reason for this is that value could be provided at runtime, so the compiler will never get the chance to optimise it. I guess it would be possible to make each work with numbers or array-likes at runtime but then everyone would pay for the feature.

I like the idea of adding support for range syntax to the each block rather than adding a new range block.

I agree. I’ll close this in favour of #8348

I would simply follow #8348’s idea and just allow omitting the as keyword. I would not introduce yet another piece of new syntax. Svelte already has a big list of new syntax and semantics. Lets take Golang as an example, they also managed having only one structure for iterating and looping. And I liked Svelte for also having just one structure ({#each}). If you look into Javascript, you’ll see that in modern projects, all you need is for as it can actually do 99% of all necessary iterations and loops. And JS’ for loop does that by allowing multiple ways of describing the for loop - and it works. Why not do the same in Svelte?

Just another syntax; rust start..end represents an open range while start..=end is a closed range

// 1 2 3 4
{#range 1..5 as n}
  {n}
{/range}

// 1 2 3 4 5
{#range 1..=5 as n}
  {n}
{/range}

Edit: a gist of all proposals so far Edit 2: There’s parallels with https://github.com/microsoft/TypeScript/issues/15480

Just to add to this discussion, I am currently using a similar approach, to what @antony described in his post, except I extracted it into a separate helper file for a nicer syntax.

range.js

export default function range(from, to) {
	if (!(to > from)) throw ("first argument must be smaller then second one");
	if (from !== parseInt(from) || to !== parseInt(to)) throw ("arguments must be of type integer");
	if (from < 0 || to < 0) throw ("arguments must be positive");

	var elements = to - from + 1;
	return [...Array(elements)].map((_, i) => i += from);
}

The range function accepts a positive range of numbers. To use it, you only need to import it and give it a range.

<script>
 	import range from "range.js"
</script>

{#each range(1, 3) as i}
	<p>{i}</p>
{/each}

I’m new too, but I’m with @sahajre on this. Since it’s a compiler, how about adding ranges as a syntax, eg. {#each [0..4] as n} which would be compiled into a vanilla for (n = 0; n <= 4; n += 1) {... loop, similar to how CoffeeScript does it.

i’d like the idea using this syntax {#range 5 as n}. Maybe we can refrence vue v-for with range ?

https://vuejs.org/v2/guide/list.html#v-for-with-a-Range

Any updates?

{#range from, to}

{/range}
{#range 1, 2}

{/range}
<!-- optional -->
{#range from, to (index)}

{/range}
{#range 1, 2 (i)}
  <p>{i}</p>
{/range}

Just to add to this discussion, I am currently using a similar approach, to what @antony described in his post, except I extracted it into a separate helper file for a nicer syntax.

range.js

export default function range(from, to) {

I think adding some flavor of range functions into the standard library along with some documentation would be quite natural. Not sure if they should return arrays, iterators or versions for both.

Complications: Ruby’s operator also handles decrementing ranges (5...1) and strings ('a'...'z' and 'z'...'a'), so if we were to steal that syntax then presumably we should also support those.

It doesn’t. (5..4) intentionally means empty range, and it really needs to for ranges to work properly.

A few languages like CoffeeScript tried to have one range type go both ways, but then there’s no way to represent empty range edge case, and resulting code gets crazy complex.

It seems as if the helper method approach https://github.com/sveltejs/svelte/issues/894#issuecomment-345469692, with a quick array check first, wouldn’t be much of an overhead at all for components which each over arrays, and for standalone components? It certainly would be much more efficient than converting to an array first, for each over iterables.

EDIT: tagging https://github.com/sveltejs/svelte/issues/4289#issuecomment-585870261

I’m not at all a fan of {#each Array in range ...} since it reminds me of looking up AngularJS 1’s syntax for each, every single time I wanted to use it, since it was pretty much a language in itself which was ultra-flexible and ultra-obtuse.

If we want to expand the API surface with {#range} then that makes sense, personally I do:

{#each [ ...Array(6) ].map(i=>0) as myThing} 

which isn’t glamorous, but it is nice to have a single purpose operation.

Overall though, I’m happy with the status-quo.

Overloading JS syntax could complicate editor support.

E.g. proposed [begin,end) for open ended range would behave like this in WebStorm: user types [. ] is auto-inserted after cursor. User continues writing begin and end which are highlighted as an array and then they need to replace ] with ).

each block is already the most syntactically complicated one. I personally like proposed range block with dot syntax. Yes, it also iterates, but even in pure JS we have at least two looping constructs depending how you count and they all have different purposes.

Another option to consider is replacing dots with ‘to’ & until. This would align with ‘as’ keyword I guess.

I don’t think people should need to understand the iterator protocol to write a for loop in a svelte template. Iterators are also slow (although they have improved over time and will continue to do so).

@pngwn, yes I see… but there’s nothing wrong with shipping something that can be done in userspace, per se.

How about {#range} just accepting the iterator protocol instead of array-like?

The user can then use any iterator they want, including an efficient iter_range generator like I used in my REPL. You can even provide it as a built-in that people can import to save them the trouble from making their own.

I’m think keeping the each syntax for ranges is inadvisable, they are completely different constructs.

Using a range function that returns an array is not really an option, that would be very inefficient and is already achievable in userland as you have shown. The whole point of the range syntax is to avoid creating unnecessary arrays.