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

Proposal - Make can-stache even more like JavaScript #528

Closed
justinbmeyer opened this Issue May 31, 2018 · 9 comments

Comments

Projects
None yet
4 participants
@justinbmeyer
Contributor

justinbmeyer commented May 31, 2018

This was discussed on a recent live stream (10:42), with follow-up discussion at 55:36.

Tasks

  • Prototype for(of)
  • Prototype let foo=bar,zed=ted
  • Make sure it works with can-stache-bindings. I think writing to these variables needs to be prevented unless defined.
  • Make sure it works with can-component
  • Allow let to be used as a place holder.
  • Figure out what to do with leakScope.
  • Update all examples to use this.key?

Problem and Solutions

Learning stache can still be a step that gets in the way of fully enjoying CanJS. I'd like to simplify it in a few ways while maintaining as much backward compatibility as possible.

In short, I'd like to fix the following problems:

  • Hard to remember each syntax
  • Weird scope lookup rules. {{../../}}
  • ViewModels use this.fullName, why doesn't can-stache?

Until we have JSX integration and it has the abilities to things like two-way binidngs, we should continue making stache a great template engine.

Hard to remember each syntax

I find myself having to take a moment to remember {{# each(todos, todo=value) }}. I'd like to support {{# for(todo of todos) }} in stache:

Instead of:

{{# each(users, user=value) }}
  <div>
     {{user.name}}
     <ul>
       {{# each(user.todos, todo=value) }}
         <li>{{todo.name}}<li>
       {{/ each }}
     </ul>
  </div>
{{/ each }}

It would be:

{{# for(user of users) }}
  <div>
     {{user.name}}
     <ul>
       {{# for(todo of user.todos) }}
         <li>{{todo.name}}<li>
       {{/ for }}
     </ul>
  </div>
{{/ for }}

Weird scope lookup rules

Things get weird if you have nested loops in CanJS. Especially with 4.0, the use of ../ or scope.top is necessary.

For example, if there was an isOwner() method on the ViewModel that needed a user and todo:

{{# each(users, user=value) }}
  <div>
     {{user.name}}
     <ul>
       {{# each(user.todos, todo=value) }}
         <li>{{todo.name}} {{../../isOwner(../user, todo) }}<li>
       {{/ each }}
     </ul>
  </div>
{{/ each }}

{{../../isOwner(../user, todo) }} ... GROSS!

Instead, I'd like to make CanJS able to walk up "closure" scopes automatically:

{{# for(user of users) }}
  <div>
     {{user.name}}
     <ul>
       {{# for(todo of user.todos) }}
         <li>{{todo.name}}  {{ this.isOwner(user, todo) }}<li>
       {{/ for }}
     </ul>
  </div>
{{/ for }}

Use this.

At the last DoneJS Chicago, we had users write something similar to:

can.Component.extend({
  tag: "evil-tinder",
  view: ` <img src="{{currentProfile.img}}"/> `,
  ViewModel: {
    profiles: {
      default(){ return [...]; }
    },
    get currentProfile() {
      return this.profiles.get(0);
    }
  }
});

They wondered why the ViewModel had this.profiles, but
the view was currentProfile.img and not this.currentProfile.img. I would like to start using this. to refer to the viewModel. Fortunately, this is already the case in stache if you aren't in a helper. The following already works:

can.Component.extend({
  tag: "evil-tinder",
  view: ` <img src="{{this.currentProfile.img}}"/> `,
  ViewModel: {
    profiles: {
      default(){ return [...]; }
    },
    get currentProfile() {
      return this.profiles.get(0);
    }
  }
});

for() loops wouldn't change the current "context", they would only add "closure" scopes. "Closure scopes" is why this would work in this.isOwner(user, todo):

{{# for(user of users) }}
  <div>
     {{user.name}}
     <ul>
       {{# for(todo of user.todos) }}
         <li>{{todo.name}}  {{ this.isOwner(user, todo) }}<li>
       {{/ for }}
     </ul>
  </div>
{{/ for }}

But it would also work outside of the for loops as it always has:

Hello, you have  {{this.users.length}}.
{{# for(user of this.users) }}
  <div>
     {{user.name}}
     <ul>
       {{# for(todo of user.todos) }}
         <li>{{todo.name}}  {{ this.isOwner(user, todo) }}<li>
       {{/ for }}
     </ul>
  </div>
{{/ for }}

Variables

I'd like to be able to create variables too:

{{let metaData = this.users.metaData}}

Hello, you have  {{this.users.length}}.
{{# for(user of this.users) }}
  <div>
     {{user.name}} is part of {{metaData.clan}}
     <ul>
       {{# for(todo of user.todos) }}
         <li>{{todo.name}}  {{ this.isOwner(user, todo) }}<li>
       {{/ for }}
     </ul>
  </div>
{{/ for }}

Or should they work with blocks:

{{# let metaData = this.users.metaData, zed = this.somethingElse}}

  Hello, you have  {{this.users.length}}.
  {{# for(user of this.users) }}
    <div>
       {{user.name}} is part of {{metaData.clan}}
       <ul>
         {{# for(todo of user.todos) }}
           <li>{{todo.name}}  {{ this.isOwner(user, todo) }}<li>
         {{/ for }}
       </ul>
    </div>
  {{/ for }}

{{/ let }}

The should probably replace scope.vars in this use case:

{{#let selectedDriver}}
  <drivers-list selected:to="selectedDriver"/>
  <driver-edit driver="selectedDriver"/>
{{/let}}

Imports

It would be nice to import as a variable. The following are some ideas:

Import via magic tags:

{{import foo from "bar"}}

Give can-import the ability to update a variable scope

<can-import let:foo="module.default"/>

I think the syntax here is weird. But this is just to give an example. Perhaps we need a version of :to that can write to variables generically:

<my-component thing:to:let="foo">

NOTE: I don't think we need this as we will make sure that thing:to can write to a variable if it is declared.

Slots

view: `
  <can-slot name="bar" variableName:from="this.viewModelName"/>
`

With leak scope?

Say we have the following, where <scope-leaker> has leakScope: true:

{{# for(thing of things) }}
  <scope-leaker/>
{{/ for }}

And scope-leaker's view looks like:

tag: "scope-leaker",
view: `
  {{thing}}
`

Should thing be able to be read?

Technical Considerations

  • We could create this as a separate project. can-jsh, can-jashe.
  • If we keep this in can-stache, {{#each}} and other helpers that change the context (this) would be "hidden" from our docs and deprecated. They would probably last util CanJS 7, possibly beyond that.
@justinbmeyer

This comment has been minimized.

Contributor

justinbmeyer commented Jun 1, 2018

Related: #505

@eben-roux

This comment has been minimized.

Contributor

eben-roux commented Jun 2, 2018

I like this.

"Traditionally" a for construct would be an incremental loop whereas foreach is a pretty established pattern. I would like foreach(user in users). That is the c# syntax. I guessing since "for each" entry "in" the collection it executes the block. If one takes the view that the array is a set then it does make sense to perform the block "for each" item "of" the set.

@justinbmeyer

This comment has been minimized.

Contributor

justinbmeyer commented Jun 5, 2018

Another gross example from bitballs:

<tbody>
	{{# each(games.getRoundsWithGames()) }}
		<tr>
			<td>{{ . }}</td>
			{{# each(../courtNames) }}
				<td>
					{{# ../../games.getGameForRoundAndCourt(.. , .) }}
						<a href="{{ routeUrl(gameId=id) }}">
							{{# ../../../teams.getById(homeTeamId) }}{{ color }}{{/ ../../../teams.getById }}
							v
							{{# ../../../teams.getById(awayTeamId) }}{{ color }}{{/ ../../../teams.getById }}
						</a>
						{{# if(scope.vm.isAdmin) }}
							<button type="button" class="btn btn-danger btn-xs"
								on:click="deleteGame(.)"
								{{# isDestroying() }}disabled{{/ isDestroying }}>
									<span class='glyphicon glyphicon-remove'/>
							</button>
						{{/ if }}
					{{/ ../../games.getGameForRoundAndCourt }}
				</td>
			{{/ each }}
		</tr>
	{{ else }}
		<tr><td class="text-center lead" colspan="5">No Games</td></tr>
	{{/ each }}
</tbody>

This becomes:

<tbody>
	{{# for( round of this.games.getRoundsWithGames() ) }}
		<tr>
			<td>{{ round }}</td>
			{{# for( courtName of this.courtNames) }}
				<td>
					{{# for( game of this.games.getGameForRoundAndCourt(round , courtName ) ) }}
						<a href="{{ routeUrl(gameId=game.id) }}">
							{{ this.teams.getById(game.homeTeamId).color }}
							v
							{{ this.teams.getById(game.awayTeamId).color }}
						</a>
						{{# if(this.isAdmin) }}
							<button type="button" class="btn btn-danger btn-xs"
								on:click="this.deleteGame(game)"
								{{# game.isDestroying() }}disabled{{/}}>
									<span class='glyphicon glyphicon-remove'/>
							</button>
						{{/ if }}
					{{/ }}
				</td>
			{{/ each }}
		</tr>
	{{ else }}
		<tr><td class="text-center lead" colspan="5">No Games</td></tr>
	{{/ each }}
</tbody>
@janat08

This comment has been minimized.

janat08 commented Jun 8, 2018

What about going full on with custom element principles and adopting https://forums.donejs.com/t/weston-a-new-templating-engine-for-canjs-apps/468/4 skipping jsx, with react essentially doing same "ungodly" stuff like HOC '<hoc my comp.../hoc>. Cudnt esc tags

justinbmeyer added a commit that referenced this issue Aug 22, 2018

@justinbmeyer

This comment has been minimized.

Contributor

justinbmeyer commented Aug 22, 2018

@janat08 please create another proposal for what you are suggesting.

@justinbmeyer

This comment has been minimized.

Contributor

justinbmeyer commented Aug 22, 2018

The scope lookup rule notes:

  • notContext - Prevents ../ and such from accessing.
  • viewModel - A special label, used so we can lookup the viewModel.
  • TemplateContext - A special context instance type that is used to house variables for the template. scope.X is used to look here.

Essentially, I think a basic "key" lookup like {{foo}} will look in (and walk) any variable scopes, and then look in the first non-variable, non-notContext scope it finds.

@matthewp

This comment has been minimized.

Contributor

matthewp commented Aug 23, 2018

I don't understand what imports is doing from the example. Why is one of them an element and one a stache helper? What's the difference between the two?

@justinbmeyer

This comment has been minimized.

Contributor

justinbmeyer commented Aug 23, 2018

@matthewp it was just showing two different possibilities.

@justinbmeyer

This comment has been minimized.

Contributor

justinbmeyer commented Sep 4, 2018

This should probably work:

{{#let selectedDriver}}
  <drivers-list selected:to="selectedDriver"/>
  <driver-edit driver="selectedDriver"/>
{{/let}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment