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

{#range ...} block #2968

Closed
Rich-Harris opened this issue Jun 7, 2019 · 51 comments
Closed

{#range ...} block #2968

Rich-Harris opened this issue Jun 7, 2019 · 51 comments
Labels
awaiting submitter needs a reproduction, or clarification feature request popular more than 20 upthumbs

Comments

@Rich-Harris
Copy link
Member

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?

@paulocoghi
Copy link
Contributor

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

@mrkishi
Copy link
Member

mrkishi commented Jun 7, 2019

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...

@pngwn
Copy link
Member

pngwn commented Jun 7, 2019

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

@joeprabawa
Copy link

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

@onkel-dirtus
Copy link

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

@iambudi
Copy link

iambudi commented Jun 12, 2019

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

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

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

@Conduitry
Copy link
Member

{#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.

@tivac
Copy link
Contributor

tivac commented Jun 12, 2019

@Conduitry why can't the compiler statically determine that {#each 5 as n} is a range? 5 isn't a valid JS identifier, so that seems like a good place to start for figuring out if the user specified a range or an array to me.

I don't much care if range support gets added, but always curious about the static analysis bits.

@pngwn
Copy link
Member

pngwn commented Jun 12, 2019

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.

@tivac
Copy link
Contributor

tivac commented Jun 12, 2019

@pngwn that's what is being asked for by at least some of the proposals though, a literal number. The compiler could easily identify that value and write out range code instead of array iteration code.

@pngwn
Copy link
Member

pngwn commented Jun 12, 2019

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.

@sahajre
Copy link

sahajre commented Jun 17, 2019

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.

@Conduitry Conduitry added awaiting submitter needs a reproduction, or clarification proposal labels Jun 22, 2019
@buhrmi
Copy link
Contributor

buhrmi commented Jun 22, 2019

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.

@OliverKarlsson
Copy link

I also prefer @sahajre's #each. However if the syntax needs to be changed, i would like to submit #do with the same rules as a candidate.

@ghost
Copy link

ghost commented Aug 27, 2019

I'll also vote for keeping an #each with altered syntax.
Please stick to either half-open interval, i.e. [begin,end) or closed interval [first,last] only.
I think having both will open up more chances for bugs and more questions.

What about adding a keyword range(begin,end,step) like Python and other languages?
We could translate this literally into an internal call that will return an array with the specified elements.
The danger is that there may be code out there that has already worked around the lack of good range syntax and has implemented a range function in their stack.

EDIT:
What if we just added the range function to the Svelte run-time and people could just import it?
Range REPL

@pngwn
Copy link
Member

pngwn commented Aug 27, 2019

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.

@ghost
Copy link

ghost commented Aug 27, 2019

@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.

@pngwn
Copy link
Member

pngwn commented Aug 27, 2019

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).

@buhrmi
Copy link
Contributor

buhrmi commented Aug 28, 2019

{#for 1..5 as n}

@tomblachut
Copy link

tomblachut commented Aug 28, 2019

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.

@ghost
Copy link

ghost commented Sep 6, 2019

maybe for and while loops.
like -

{#iterate-for i in range(begin,end,step)}
<box>
{/iterate}

while the default begin is 0 and step is optional.
(similar to python syntex)
and :

{#iterate-while (condition)}
<box>
{/iterate}

where the condition could be something connected to the dom , like
"iterate until the viseable part of the screen is full"
or "iterate until the elements block height is bigger then 1000px"
or "iterate until the element block width is 75% from the screen"
or combine them :
"iterate until the elements block height is 1000px and the element block width is 75% from the screen"

@tomcon
Copy link

tomcon commented Nov 27, 2019

#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}

@tomcon
Copy link

tomcon commented Dec 1, 2019

#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.

@jdgaravito
Copy link

i'm looking forward for this feature, i would prefer a modified {#each} rather than {#range} but with a ruby's flavor.

maybe {#each Array in range 1 to 10 as i } ?

@tomcon
Copy link

tomcon commented Jan 4, 2020

Waited for some movement in this one for quite a while but it's gone very quiet so ...

@Rich-Harris @Conduitry Is this dead in the water or, does switching to #for from #each seems a natural and seamless (and more js) thing to do, with new benefits and throwing off finally the old handlebars #each as legacy syntax.

#each would be supported but marked as deprecated once #for was released.

Also, compare the syntax of the current workarounds for #each weaknesses and then is not the argument for #for even more compelling?

@pngwn
Copy link
Member

pngwn commented Jan 4, 2020

#for would be the same as #each, since you are just suggesting a keyword change so the same limitations would apply, and that definitely won’t be happening until at least version 4 if at all. We won’t support two syntaxes to achieve the same thing so such a change would be breaking. Swapping a!keyword isn’t a compelling enough reason either to release a new major or to break our rule of not duplicating functionality.

That said, this isn’t dead in the water (or it would have been closed), we just have limited bandwidth.

@tomcon
Copy link

tomcon commented Jan 5, 2020

@pngwn not just a keyword change, see previous comment 4 back & the wish to support ranges.

Perhaps it's just too tricky as @Rich-Harris previously said in #894

Going to close this as it adds way too much complexity and overhead given for something you can already do trivially with each Object.entries(obj) and each [...iterable]. Better to have one clear way of doing things rather than multiplying the stuff that needs to be maintained and documented.

@rlaferla
Copy link

rlaferla commented Feb 7, 2020

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".

@mikebeaton
Copy link

mikebeaton commented Feb 13, 2020

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!

@mikebeaton
Copy link

mikebeaton commented Feb 13, 2020

It seems as if the helper method approach #894 (comment), 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 #4289 (comment)

@MartinElsaesser
Copy link

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}

@rottmann
Copy link

rottmann commented Jul 17, 2020

A slightly broader functionality of @MartinElsaesser range-function

Ranges positive and negative numbers and alphabetical chars (first char only).
With an option to return an empty array if from and to are the same value.

function range(from, to, includeFrom = true) {
  let isString = false

  if (from !== parseInt(from) || to !== parseInt(to)) {
    if (typeof from === 'string' && typeof to === 'string') {
      isString = true
      from = from[0].charCodeAt(0)
      to = to[0].charCodeAt(0)
    } else {
      throw (`'range' arguments must be of type integer or string`);
    }
  }

  const distance = Math.abs(to - from)
  const length = distance + ((includeFrom || distance > 0) ? 1 : 0)

  if (isString) {
    if (from > to) {
      return elements.map( (_, i) => String.fromCharCode(from - i) )
    }
    return elements.map( (_, i) => String.fromCharCode(from + i) )
  } else {
    if (from > to) {
      return elements.map( (_, i) => from - i)
    }
    return elements.map( (_, i) => from + i)
  }
}

Examples

range(1, 3) // [ 1, 2, 3 ]
range(3, 1) // [ 3, 2, 1 ]
range(-1, -3) // [ -1, -2, -3 ]
range(-3, -1) // [ -3, -2, -1 ]
range(-3, 3) // [ -3, -2, -1, 0, 1, 2, 3 ]
range(1, 1) // [ 1 ]
range(1, 1, false) // [ ]
range('a', 'c') // [ 'a', 'b', 'c' ]
range('c', 'a') // [ 'c', 'b', 'a' ]

But a JS for-loop {#for ...}...{/for} would feel natural to use as the range hack.

@rosslh
Copy link

rosslh commented Jul 21, 2020

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)

@taw
Copy link

taw commented Sep 27, 2020

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.

@aradalvand

This comment has been minimized.

@mbacon-edocs
Copy link

mbacon-edocs commented May 13, 2021

Just weighing in to say it sounds like an ace idea, am pro the syntaxes of {#range 5 as n} {#range 1..5 as n} {#range myVar as n} and same but with {#each prefix..

Main extra point - if adding support for this, please include a step parameter, so we can do increments of 5 from 0-100 {#range 0..100 as n step 5} or similar so we get 0,5,10,15... forcing increments of 1 feels too limiting

@pngwn pngwn added popular more than 20 upthumbs feature request and removed proposal labels Jun 26, 2021
@aradalvand
Copy link

aradalvand commented Jul 8, 2021

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}

@braedenf
Copy link

braedenf commented Oct 15, 2021

Could do a similar approach to Haskell like this:

{#range 1, 5..50 as i} <!-- use comma to denote step and use .. for range specification  -->
    <!-- Do stuff... -->
{/range}

@WHenderson
Copy link

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

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

If we are to offer a syntax that supports the iterator protocol, I would suggest we also provide the index much like the current {#each} block does. i.e.

{#for thing, index of iterable}
  {index}:{thing}
{/for}

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

@WHenderson
Copy link

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.

@aradalvand
Copy link

aradalvand commented Dec 6, 2021

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.

@icecream17
Copy link

icecream17 commented Feb 9, 2022

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 microsoft/TypeScript#15480

@brunnerh
Copy link
Member

brunnerh commented Mar 2, 2022

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#)

@smithliam
Copy link

{#range from, to}

{/range}
{#range 1, 2}

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

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

@jude-ui

This comment was marked as spam.

@weepy
Copy link

weepy commented Dec 8, 2022

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

@KevsRepos
Copy link

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?

@aradalvand
Copy link

aradalvand commented Jul 5, 2023

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}

@Rich-Harris
Copy link
Member Author

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

@Rich-Harris Rich-Harris closed this as not planned Won't fix, can't repro, duplicate, stale Apr 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting submitter needs a reproduction, or clarification feature request popular more than 20 upthumbs
Projects
None yet
Development

No branches or pull requests