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

Template syntax for let bindings #1521

Closed
girving opened this issue Jun 4, 2018 · 17 comments
Closed

Template syntax for let bindings #1521

girving opened this issue Jun 4, 2018 · 17 comments

Comments

@girving
Copy link

girving commented Jun 4, 2018

Currently there is no specific syntax to reuse computations that depend on each loop variables. For example, here's some code that loops over some numbers and uses the squares of the numbers in two different ways:

{#each [1,2,3,4] as n}
  {#each [n*n] as sqr_n}
    {sqr_n} {sqr_n/2}<br>
  {/each}
{/each}

The inner each is a hack to define a reusable computation. It would lovely to have syntax something like this:

{#each [1,2,3,4] as n}
  {#let sqr_n = n * n}
  {sqr_n} {sqr_n/2}<br>
{/each}

Besides being easier to read, that syntax has the advantage of not introducing another nesting level. Is there a clean alternative to the second version in current svelte? If not, would this be a reasonable feature to add?

@Conduitry
Copy link
Member

A reasonably clean alternative would be to map a function over the array and use destructuring in the each loop:

{#each [1, 2, 3, 4].map(n => ({ n, sqr_n: n * n })) as { n, sqr_n }}
	{sqr_n} {sqr_n / 2}<br>
{/each}

@girving
Copy link
Author

girving commented Jun 4, 2018

That avoids the extra nesting level, but I still find the let version easier to read.

@corneliusio
Copy link

corneliusio commented Jun 5, 2018

Seems to me that the far better direction here would simply be to use a helper for this sort of situation.

{#each [1,2,3,4] as n}
  {sqr(n)} {sqr(n)/2} <br>
{/each}

<script>
  export default {
    helpers: {
      sqr(n) {
        return n * n;
      }
    }
  };
</script>

Or, if for some reason you didn't want to perform the operation twice per loop, you could simply setup a computed property and loop over the numbers.

{#each squares as sqr}
  {sqr} {sqr / 2} <br>
{/each}

<script>
  export default {
    data() {
      return {
        numbers: [1, 2, 3, 4]
      };
    },

    computed: {
      squares(numbers) {
        return numbers.map(n => n * n);
      }
    }
  };
</script>

Just my two cents. I think either seem to be a way clearner solution then the nested loop or the inlined map option.

@girving
Copy link
Author

girving commented Jun 5, 2018

TOOAWTDI: There's only one (awkward) way to do it. :)

@Rich-Harris
Copy link
Member

Generally my experience has been that when you find yourself wanting a feature like this, it's a sign that it's time to break your component up into multiple components:

{#each [1,2,3,4] as n}
  <Item {n} sqr_n={n * n}/>
{/each}

@girving
Copy link
Author

girving commented Jun 5, 2018

@Rich-Harris Could I get your intuition for why that rule of thumb applies to svelte components but not Javascript functions? I tend to make heavy use of let x = e when writing normal Javascript, as I do in most other languages (though unlambda is a notable exception). How is svelte different?

@Rich-Harris
Copy link
Member

@girving not one that bears any real scrutiny 😀 It could be nothing more than my mind coming up with ways to justify the absence of variable declarations, because the thought of implementing them scares me. But I'll try:

When you write imperative code, you're writing sequences of statements that execute in order. So this...

let x = a;
let y = x;

...means something different to this:

let y = x;
let x = a;

(Feel free to substitute in an example that doesn't involve a TDZ violation if it makes more sense.)

In a declarative language like Svelte's HTMLx, you're not writing a program so much as describing a graph. The relationships between the different parts of the graph don't quite map to the relationships between different JavaScript statements. Given that, the semantics of any form of variable declaration would inevitably differ from JavaScript's semantics enough to cause real confusion. I think the only way it would really make sense is if it was another block helper, like this...

{#each [1,2,3,4] as n}
  {#with sqr_n = n * n}
    {sqr_n} {sqr_n/2}<br>
  {/with}
{/each}

...but that starts to look a bit unwieldy.

When I say that my experience is that it means it's time to split up your components, I guess I mean that there tends to be a logical grouping between all the things that care about (for example) sqr_n, and in Svelte, logical groupings are expressed as components.

@girving
Copy link
Author

girving commented Jun 8, 2018

I'm imagining this let / with construct from the perspective of ocaml and similar functional languages, where you say

let x = e in

and the scope implicitly extends as far as appropriate. This differs from your with example only in that the end would be implicit. In particular, at the code level it differs only at parsing time.

Happy to close if you think this isn't worth the trouble. Similar to Javascript or ocaml it is always possible to replace let with a new function, so as you say there are ways around the lack of syntax.

@lowi
Copy link

lowi commented Aug 31, 2018

I am using the store to compute 3x arrays on which I am looping like this:

{ #each $preTasks as preTask }
{/each}

{#each $tasks as task}
{/each}

{#each $postTasks as postTask}
{/each}

// And I have something like this on the store:
store.on('state', ({changed}) => {
const preTasks = ['stuff'], tasks = ['stuff'], postTasks = ['stuff'];
store.set({preTasks, tasks, postTasks})
})

In this case, is the template updated in one loop even if I use 3x different each loops?

Having let would allow me to have something like this:

{#let mytasks=$mytasks}
{ #each mytasks.preTasks as preTask }
{/each}

{#each mytasks.tasks as task}
{/each}

{#each mytasks.postTasks as postTask}
{/each}
{/let}

// And I would have something like this on the store:
store.on('state', ({changed}) => {
const preTasks = ['stuff'], tasks = ['stuff'], postTasks = ['stuff'];
store.set({myTasks: {preTasks, tasks, postTasks}})
})

In this case, the code seems more readable to me as I am grouping my data together (But it's true I could create a sub-component that would receive myTasks).

@Rich-Harris
Copy link
Member

In this case, is the template updated in one loop even if I use 3x different each loops?

Yes — changes that happen in the same set are all applied simultaneously — in other words these two are not the same:

// results in a single update
store.set({preTasks, tasks, postTasks})

// results in three separate updates
store.set({preTasks})
store.set({tasks})
store.set({postTasks})

Having said that, Svelte does what it can to avoid doing unnecessary work even in the second case; for example when you change preTasks, it won't re-render the tasks block with the same data as before.

@PaulMaly
Copy link
Contributor

PaulMaly commented Sep 2, 2018

@Rich-Harris Btw, with-block in Ractive is a very great thing. I would be glad to get it in Svelte too.

I lack a little an opportunity to create the internal context without the need for the creation of one more component. My experience is that components count increase very fast in Svelte, much faster than in Ractive.

@ahopkins
Copy link

ahopkins commented Dec 26, 2019

For further rationale for wanting something like this (in-block evaluation), see a related issue. Yes, it can be achieved with multiple components (see link), but it is potentially a lot more verbose and messy than it could be with another block.

@milahu
Copy link
Contributor

milahu commented Jan 1, 2020

A reasonably clean alternative would be to map a function over the array and use destructuring in the each loop:

{#each [1, 2, 3, 4].map(n => ({ n, sqr_n: n * n })) as { n, sqr_n }}
	{sqr_n} {sqr_n / 2}<br>
{/each}

yes! this should be the "best practice" here

a benchmark shows no difference, at least for small data
so the only argument is "eye candy"

here is a TreeView component, using

{#each Object.entries(object)
  .map(([key, val]) => [key, val,
    joinPath(path, key), get_pathDot(path)])
  as [key, val, keyPath, pathDot]
}

it is a bit redundant
but the advantage is the "functional style"
with its strict separation of scopes
= less attack surface for bugs

for large or unknown objects
we can use Object.assign like

{#each array.map((obj) => Object.assign(
  obj, {newProp: obj.a + obj.b}
  )) as obj
}

Object.assign is more explicit than object destructuring
so more readable, more beginner friendly

IMO we should add this to the tutorial
probably as an extra chapter "advanced each blocks"

otherwise the template system looks too limiting
when beginners see the trivial examples
where "javascript expressions" are only variables

IMO svelte does have a responsibility
to teach/demo the basics of "functional javascript"
probably as a docs/tutorial/demos chapter
on "the power of javascript expressions"

@ahopkins
Copy link

ahopkins commented Jan 2, 2020

@milahu The problem is with something like this, you cannot really achieve the lazy evaluation that I was attempting in the example I linked to. Yes, I suppose it could be done inside the map, and then a filter, etc.... But this is starting to become rather verbose at what could be a much simpler and more elegant solution if only there were another template helper that could do variable assignment.

@milahu
Copy link
Contributor

milahu commented Jan 2, 2020

lazy evaluation

here is a solution : D

{#each [obj.get(obj)] as val}
  val = {val}
{/each}

create a "pseudo list" with just one item [getItem(key)]
and loop over that list

a {#with} block only saves two parentheses
but its easier to see "what is happening here"

// proposal
{#with obj.get(obj) as val}
  val = {val}
{/with}

the equal sign = is reserved for reactive assignments
hence the expr as val syntax

/*
  with block implementation sample

  {#with getVal() as val}
    local: val = {val}
  {/with}

  variable shadowing in local scope
  val = getVal()
  IIFE = Immediately Invoked Function Expression
*/

let val = 'global value'
const getVal = () => 'local  value'
const p = (str) => console.log(str)
let res = null

// with Grouping Operator ()
p('1. global val = '+val)
res = ( function (val) {
    // block contents
    p('2. local  val = '+val)
    return 'local  result 1'
} )(getVal());
p('3. global res = '+res)
p('3. global val = '+val)

// without Grouping Operator ()
res = function (val) {
    // block contents
    p('4. local  val = '+val)
    return 'local  result 2'
}(getVal())
p('5. global res = '+res)
p('5. global val = '+val)

result

1. global val = global value
2. local  val = local  value
3. global res = local  result 1
3. global val = global value
4. local  val = local  value
5. global res = local  result 2
5. global val = global value

@antony
Copy link
Member

antony commented Apr 9, 2020

Closing in favour of the with and range discussions.

@Zachiah
Copy link
Contributor

Zachiah commented Aug 14, 2023

For anyone coming across this. They have introduced an @const tag

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests