├── .gitignore ├── 0000-template.md ├── README.md └── text ├── 0001-reactive-assignments.md ├── 0002-reactive-stores.md ├── 0003-reactive-declarations.md ├── 0004-better-composition.md ├── 0005-svelte-component-interface.md ├── 0006-style-properties.md ├── 0007-markup-constants.md └── 0008-style-directives.md /.gitignore: -------------------------------------------------------------------------------- 1 | /scratch -------------------------------------------------------------------------------- /0000-template.md: -------------------------------------------------------------------------------- 1 | - Start Date: (fill me in with today's date, YYYY-MM-DD) 2 | - RFC PR: (leave this empty) 3 | - Svelte Issue: (leave this empty) 4 | 5 | # (RFC title goes here) 6 | 7 | ## Summary 8 | 9 | > One paragraph explanation of the feature. 10 | 11 | ## Motivation 12 | 13 | > Why are we doing this? What use cases does it support? 14 | 15 | > Please provide specific examples. If you say "this would be more flexible" then 16 | > give an example of something that becomes easier. If you say "this would be make 17 | > it easier to do X" then give an example of what that looks like today and what's 18 | > hard about it. 19 | 20 | > Don't assume that others recognize the problem is one that needs to be solved 21 | > Is there some concrete issue you cannot accomplish without this? 22 | > What does it look like to accomplish some set of goals today and how could 23 | > that be improved? 24 | > Are there any workarounds that are necessary today? 25 | > Are there open issues on Github where people would be helped by this? 26 | > Will the change have performance impacts? Can you quantify them? 27 | 28 | > Please focus on explaining the motivation so that if this RFC is not accepted, 29 | > the motivation could be used to develop alternative solutions. In other words, 30 | > enumerate the constraints you are trying to solve without coupling them too 31 | > closely to the solution you have in mind. 32 | 33 | ## Detailed design 34 | 35 | ### Technical Background 36 | 37 | > There are a lot of ways Svelte is used. It's hosted on different platforms; 38 | > integrated with different libraries; built with different bundlers, etc. No one 39 | > person knows everything about all the ways Svelte is used. What does someone who 40 | > knows about Svelte but hasn't necessarily used anything outside of it need to 41 | > know? Are there docs you can share? 42 | 43 | > How do different libraries or frameworks implement this feature? We can take 44 | > design inspiration from others who have done this well and improve upon the 45 | > designs or make them better fit Svelte. 46 | 47 | ### Implementation 48 | 49 | > Explain the design in enough detail for somebody familiar with the framework to 50 | understand, and for somebody familiar with the implementation to implement. Any 51 | > new terminology should be defined here. 52 | 53 | > Explain not just the final design, but also how you arrived at it. What 54 | > constraints did you face? Are there corner cases you've come up with solutions for? 55 | 56 | > Explain how your design fits into the larger picture. Are the other open problems 57 | > in this area you're familiar with? How does this design fit with potential 58 | > solutions for those issues? 59 | 60 | > Connect your design to the motivations you listed above. When describing a part of 61 | > the design, it can be useful to share an example of what it would look like to 62 | > utilize the implementation as solution to the problem. 63 | 64 | ## How we teach this 65 | 66 | > What names and terminology work best for these concepts and why? How is this 67 | idea best presented? As a continuation of existing Svelte patterns, or as a 68 | wholly new one? 69 | 70 | > Would the acceptance of this proposal mean the Svelte guides must be 71 | re-organized or altered? Does it change how Svelte is taught to new users 72 | at any level? 73 | 74 | > How should this feature be introduced and taught to existing Svelte 75 | users? 76 | 77 | ## Drawbacks 78 | 79 | > Why should we *not* do this? Please consider the impact on teaching Svelte, 80 | on the integration of this feature with other existing and planned features, 81 | on the impact of the API churn on existing apps, etc. 82 | 83 | > There are tradeoffs to choosing any path, please attempt to identify them here. 84 | 85 | ## Alternatives 86 | 87 | > What other designs have been considered? What is the impact of not doing this? 88 | 89 | > This section could also include prior art, that is, how other frameworks in the 90 | > same domain have solved this problem differently. 91 | 92 | ## Unresolved questions 93 | 94 | > Optional, but suggested for first drafts. What parts of the design are still TBD? 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte RFCs 2 | 3 | This is a place to discuss major changes to Svelte — where 'major' implies significant changes either to public interfaces or internal implementation details, particularly those that could be controversial or involve breaking changes. 4 | 5 | Most changes don't need to go through the Request for Comments (RFC) process and can rely on issues and pull requests. 6 | 7 | A huge part of the value on an RFC is defining the problem clearly, collecting use cases, showing how others have solved a problem, etc. Coming up with a design is very iterative and only one part of the process. An RFC can provide tremendous value without the design described in it being accepted. 8 | 9 | The is inspired by (which is to say shamelessly ripped off from) the RFC process adopted by [React](https://github.com/reactjs/rfcs), [Ember](https://github.com/emberjs/rfcs) and others. The process itself is subject to change (or even abandonment!) as we gain experience with it. 10 | 11 | 12 | ## The process 13 | 14 | * Fork the RFC repo http://github.com/sveltejs/rfcs 15 | * Copy 0000-template.md to text/0000-my-feature.md (where 'my-feature' is descriptive. Don't assign an RFC number yet). 16 | Fill in the RFC. Put care into the details: **RFCs that do not present convincing motivation, demonstrate understanding of the impact of the design, or are disingenuous about the drawbacks or alternatives tend to be poorly-received.** 17 | * Submit a pull request. As a pull request the RFC will receive design feedback from the larger community, and the author should be prepared to revise it in response. 18 | * Build consensus and integrate feedback. RFCs that have broad support are much more likely to make progress than those that don't receive any comments. 19 | * Eventually, the team will decide whether the RFC is a candidate for inclusion in Svelte. 20 | RFCs that are candidates for inclusion in Svelte will enter a "final comment period" lasting 3 calendar days. The beginning of this period will be signaled with a comment and tag on the RFCs pull request. 21 | * An RFC can be modified based upon feedback from the team and community. Significant modifications may trigger a new final comment period. 22 | An RFC may be rejected by the team after public discussion has settled and comments have been made summarizing the rationale for rejection. A member of the team should then close the RFCs associated pull request. 23 | * An RFC may be accepted at the close of its final comment period. A team member will merge the RFCs associated pull request, at which point the RFC will become 'active'. 24 | 25 | 26 | ## The RFC life-cycle 27 | 28 | Once an RFC becomes active, then authors may implement it and submit the feature as a pull request to the Svelte repo. Becoming 'active' is not a rubber stamp, and in particular still does not mean the feature will ultimately be merged; it does mean that the core team has agreed to it in principle and are amenable to merging it. 29 | 30 | Furthermore, the fact that a given RFC has been accepted and is 'active' implies nothing about what priority is assigned to its implementation, nor whether anybody is currently working on it. 31 | 32 | Modifications to active RFCs can be done in followup PRs. We strive to write each RFC in a manner that it will reflect the final design of the feature; but the nature of the process means that we cannot expect every merged RFC to actually reflect what the end result will be at the time of the next major release; therefore we try to keep each RFC document somewhat in sync with the language feature as planned, tracking such changes via followup pull requests to the document. 33 | 34 | 35 | ## Implementing an RFC 36 | 37 | The author of an RFC is not obligated to implement it. Of course, the RFC author (like any other developer) is welcome to post an implementation for review after the RFC has been accepted. 38 | 39 | If you are interested in working on the implementation for an 'active' RFC, but cannot determine if someone else is already working on it, feel free to ask (e.g. by leaving a comment on the associated issue). 40 | 41 | 42 | ## Reviewing RFCs 43 | 44 | We tend to do our thinking informally, in the open, when time allows. There are a large number of community members relative to a small number of a core contributors who have many responsibilities. You can help ensure your RFC is reviewed in a timely manner by putting in the time to think through the various details discussed in the template. It doesn't scale to push the thinking onto a small number of core contributors. If reviewers raise an issue, don't dismiss it as irrelevant, but take the time to provide examples or data explaining it and coming up with ways that the design might be changed in response. Sometimes answering a single question can be very time consuming (such as setting up a benchmark), but discussions tend to stall out if concerns don't get thoroughly addressed. 45 | -------------------------------------------------------------------------------- /text/0001-reactive-assignments.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2018-11-01 2 | - RFC PR: https://github.com/sveltejs/rfcs/pull/4 3 | - Svelte Issue: https://github.com/sveltejs/svelte/issues/1826 4 | 5 | # Reactive assignments 6 | 7 | ## Summary 8 | 9 | This RFC proposes a radically new component design that solves the biggest problems with Svelte in its current incarnation, resulting in smaller apps and less code to write. It does so by leveraging Svelte's unique position as a compiler to solve the problem of *reactivity* at the language level. 10 | 11 | ## Motivation 12 | 13 | Many developers report being intrigued by Svelte, but unwilling to accommodate some of the quirks of its design. For example, using features like nested components or transitions requires that you *register* them, involving unfortunate boilerplate: 14 | 15 | ```html 16 |
17 | 18 |
19 | 20 | 29 | ``` 30 | 31 | Declaring methods is also somewhat cumbersome, and in many editor configurations leads to red squigglies since it violates the normal semantics of JavaScript: 32 | 33 | ```html 34 | 35 | 36 | 47 | ``` 48 | 49 | Perhaps the biggest hangup is that the markup doesn't have access to arbitrary values inside the ` 82 | ``` 83 | 84 | There are legitimate technical and historical reasons for this design. And some people — particularly those with a background in Vue or Ractive — are familiar with it and enjoy it. But the reality is that for most of us, it sucks. It would be much nicer to do something like this: 85 | 86 | ```html 87 | 92 | 93 |

Hello {capitalise(name)}!

94 |

Your name has {len()} letters in it.

95 | ``` 96 | 97 | Why can't we? Because of *reactivity*. There's no way for the view to 'know' if `name` has changed. 98 | 99 | 100 | ## The reactivity problem 101 | 102 | UI frameworks are all, to some degree, examples of reactive programming. The notion is that your view can be thought of as a function of your state, and so when your state changes the view must, well... react. 103 | 104 | Reactivity is implemented in a variety of different ways (note: I am not expert in these frameworks other than Svelte, please correct any misapprehensions): 105 | 106 | * **React**'s approach is to re-render the world on every state change. This necessitates the use of a virtual DOM, which in turn necessitates a virtual DOM reconciler. It is effective, but an inefficient approach for three reasons: first, the developer's code must run frequently, providing ample opportunity for 'death by a thousand paper cuts' particularly if the developer is unskilled; second, the virtual DOM itself is a short-lived object, thirdly, discovering 'what changed' via virtual DOM diffing is wasteful in the common case that the answer is 'not much'. Reasonable people can (and do) disagree about whether these inefficiences are impactful on real apps (the answer is presumably 'it depends'), but it is well known that the most reliable way to make code faster is for it to do less work, and React does a lot of work. 107 | 108 | * **Vue** can track which values changed using getters and setters (accessors). In other words, your data forms the basis of a 'viewmodel', with accessors corresponding to the initial properties. Assigning to one of them — `vm.foo = 1` — schedules an update. This code is pleasant to write but is not without its downsides — it can result in unpredictable behaviour as values are passed around the app, and it encourages mutation. It also involves some minor computational and memory overhead, and some gotchas when setting as-yet-unknown properties (this will be fixed in Vue 3 with proxies, but presumably at the cost of IE support). 109 | 110 | * **Svelte** provides each component with a `set` method (and a corresponding `get` method) allowing the developer to make explicit state changes. (Those changes are fully synchronous, which is both good and bad — good because the mental model and resulting stack traces are simple, bad because it can result in layout thrashing, buggy lifecycles, and infinite loops that need to be guarded against.) This is verbose when mutating or copying objects, and puts valuable things like first-class TypeScript integration essentially out of reach. 111 | 112 | None of these are ideal. But [we're a compiler](https://mobile.twitter.com/Rich_Harris/status/1057290365395451905), which means we have tools in our toolbox that are unique to Svelte. What if we could solve reactivity *at the language level*? 113 | 114 | This proposal outlines a novel (as far as we're aware) solution, while bringing a number of significant benefits: 115 | 116 | * Easier-to-grok design 117 | * Less application code 118 | * Smaller bundles 119 | * Simpler, more robust lifecycles 120 | * A realistic path towards first-class TypeScript support 121 | * Well-defined contracts between components and their consumers 122 | * Opportunities to adopt ideas like [time slicing and Suspense](https://auth0.com/blog/time-slice-suspense-react16/) 123 | 124 | 125 | ## Detailed design 126 | 127 | Consider a counter component, the workhorse of examples such as these: 128 | 129 | ```html 130 | 133 | 134 | 137 | ``` 138 | 139 | > The quotes around the event handler are unnecessary as far as Svelte is concerned, but included for the sake of non-Svelte-aware syntax highlighting 140 | 141 | Here, we have declared a single state variable, `count`, with an initial value of `0`. The code inside the ` 163 | 164 | 167 | ``` 168 | 169 | ...would be transformed to something like this: 170 | 171 | ```js 172 | function create_main_fragment(component, ctx) { 173 | // ... the code that creates and maintains the view — 174 | // this will be relatively unaffected by this proposal 175 | } 176 | 177 | const Component = defineComponent((__update) => { 178 | let count = 0; 179 | const incr = () => { 180 | count += 1; 181 | __update({ count: true }); 182 | }; 183 | 184 | return [ 185 | // this creates the top-level `ctx` variable for `create_main_fragment` 186 | () => ({ count }) 187 | ]; 188 | }, create_main_fragment); 189 | ``` 190 | 191 | **This behaviour might seem surprising, even shocking at first.** Variable assignment does not have side-effects in JavaScript, meaning that this is arguably something else — [SvelteScript](https://mobile.twitter.com/youyuxi/status/1057291776724271104), perhaps. In practice though, developers readily embrace 'magic' that makes their day-to-day lives easier as long as the mechanisms are ultimately easy to understand — something observed with React Hooks, Vue's reactivity system, Immer's approach to immutable data, and countless other examples. This does underscore the need for this code transformation to be well-documented and explained, however. 192 | 193 | 194 | ### Mutating objects and arrays 195 | 196 | While it's good practice to use immutable data in components, Svelte should not force developers to do so. Assigning to a property of an object or array would have the same basic behaviour — `a.b = c` would trigger an update on `a`. It's possible that changes could be tracked at a more granular level (i.e. tracking `a.b` rather than `a`, so that `

{a.d}

` is unaffected), but these are implementation details beyond the scope of this RFC. 197 | 198 | Calling array methods would *not* trigger updates on those arrays. 199 | 200 | > `x = x` can be used to 'force' an update on `x`, in the rare cases it is necessary 201 | 202 | 203 | ### Props 204 | 205 | Many frameworks (and also web components) have a conceptual distinction between 'props' (values passed *to* a component) and 'state' (values that are internal to the component). Svelte does not. This is a shortcoming. 206 | 207 | There is a solution to this problem, but brace yourself — this may feel a little weird at first: 208 | 209 | ```html 210 | 213 | 214 |

Hello {name}!

215 | ``` 216 | 217 | Here, we are exporting a *contract* with the outside world — we are saying that a consumer of this component can specify a value for `name`, but that it will default to `world`: 218 | 219 | ```html 220 | 221 | ``` 222 | 223 | This would compile to something like the following: 224 | 225 | ```js 226 | const Component = defineComponent((__update) => { 227 | let name = 'world'; 228 | 229 | return [ 230 | () => ({ name }), 231 | props => { 232 | if ('name' in props) name = props.name; 233 | } 234 | ]; 235 | }, create_main_fragment); 236 | ``` 237 | 238 | **This is an abuse of syntax**, but no worse than Svelte's existing crime of abusing `export default`. As long as it is well-communicated, it is easy to understand. Using existing syntax, rather than inventing our own, allows us to take full advantage of existing tooling and editor support. 239 | 240 | > Ordinarily in JavaScript, `export` means that other consumers of this module can read the value. In a sense, that's true (as we'll learn in the [Component API](#component-api) section), but here it also allows the consumer of this module to *write* the value. That's why we say that we're exporting a *contract* rather than a *value*. 241 | 242 | To summarise, there are **3 simple rules** for understanding the code in a Svelte component's ` 272 | 273 |

The time is {format_time(time)}

274 | ``` 275 | 276 | The `onDestroy` callback is associated with the component instance because it is called during instantiation; no compiler magic is involved. Beyond that (if it is called outside instantiation, an error is thrown), there are no rules or restrictions as to when a lifecycle function is called. Reusability is therefore straightforward: 277 | 278 | ```js 279 | // helpers.js 280 | import { onDestroy } from 'svelte'; 281 | 282 | export function useInterval(fn, ms) { 283 | const interval = setInterval(fn, ms); 284 | onDestroy(() => clearInterval(interval)); 285 | fn(); 286 | } 287 | ``` 288 | 289 | ```html 290 | 296 | 297 |

The time is {formatTime(time)}

298 | ``` 299 | 300 | > This might seem less ergonomic than React Hooks, whereby you can do `const time = useCustomHook()`. The payoff is that you don't need to run that code on every single state change, and it's easier to see which values in a component are subject to change, and *when* a specific value is changing. 301 | 302 | There are three other lifecycle functions required — `onMount` (similar to `oncreate` in Svelte v2), `beforeUpdate` (similar to `onstate`) and `afterUpdate` (similar to `onupdate`): 303 | 304 | ```html 305 | 329 | ``` 330 | 331 | > Note that the `changed`, `current` and `previous` arguments, present in Svelte 2, are absent from these callbacks — I believe they are now unnecessary. 332 | 333 | Currently, `onstate` and `onupdate` run before and after every state change. Because Svelte 2 doesn't differentiate between external props and internal state, this can easily result in confusing cyclicality: 334 | 335 | ```js 336 | 356 | ``` 357 | 358 | Under this proposal, `beforeUpdate` callbacks run only once per cycle. This makes infinite loops impossible: 359 | 360 | ```html 361 | 378 | ``` 379 | 380 | > Note that `previous_temperature`, if unused in the view, will not get the reactive treatment. 381 | 382 | Any `afterUpdate` callbacks would run after the view was updated, whether as a result of prop or state changes. Assignments in an `afterUpdate` callback would result in an immediate re-render (once all `afterUpdate` callbacks have run, instead of on the next frame) but would *not* cause the callback to run again. This would allow components to respond to layout changes, for example. 383 | 384 | 385 | --- 386 | 387 | The remainder of this section will address how existing Svelte concepts are affected by this RFC. Some things (CSS etc) are unmentioned because they are unaffected, though if items are missing please raise your voice. 388 | 389 | 390 | ### Directives 391 | 392 | In Svelte 2 there are six different kinds of directive: 393 | 394 | * `on:[event]` and `on:[event]=[callExpression]` 395 | * `use:[action]` and `use:[action]=[argument]` 396 | * `ref:[name]` 397 | * `in:[transition]`, `out:[transition]` and `transition:[transition]`, and `in:[transition]=[params]` etc 398 | * `bind:[name]` and `bind:remote=[local]` 399 | * `class:[name]` and `class:[name]=[expression]` 400 | 401 | Currently, directive values do not have curly braces as delimiters. This is an occasional source of confusion (and compiler complexity) that can be resolved by making directives more like attributes: 402 | 403 | ```html 404 | 405 |
...
406 | 407 | 408 |
...
409 | ``` 410 | 411 | Even though this is two extra characters (we preserve the quotes because we care about syntax highlighting in non-Svelte-aware environments) it's arguably more readable and consistent. This pattern would be used across all directives, but there are particular changes that apply to `ref:` and `on:`, discussed below. 412 | 413 | 414 | #### `ref:` 415 | 416 | The `ref:` directive is anomalous in that it cannot have a value. This relates to a very common feature request — to be able to set refs inside `each` blocks. We can fix both things — and reduce API surface area — by replacing `ref:x` with `bind:this={x}`: 417 | 418 | ```html 419 | 420 | ``` 421 | 422 | In Svelte 2, that element could be accessed (from `oncreate` onwards) as `this.refs.canvas`. Under this proposal, refs are simple variables: 423 | 424 | ```html 425 | 438 | ``` 439 | 440 | (Since a binding can have any [l-value](https://en.wikipedia.org/wiki/Value_(computer_science)#lrvalue), we can also do `bind:this={array[index]}` inside `each` blocks.) 441 | 442 | The example above could compile to something like the following: 443 | 444 | ```js 445 | import { afterUpdate } from 'svelte'; 446 | 447 | const Component = defineComponent((__update) => { 448 | let width; 449 | let height; 450 | let canvas; 451 | let ctx; 452 | 453 | afterUpdate(() => { 454 | if (!ctx) ctx = canvas.getContext('2d'); 455 | draw_some_shapes(ctx); 456 | }); 457 | 458 | return [ 459 | () => ({ width, height }), 460 | props => { 461 | if ('width' in props) width = props.width; 462 | if ('height' in props) height = props.height; 463 | }, 464 | refs => { 465 | if ('canvas' in refs) canvas = refs.canvas; 466 | } 467 | ]; 468 | }, create_main_fragment); 469 | ``` 470 | 471 | The same would apply to component refs. 472 | 473 | 474 | #### `on:` 475 | 476 | Currently, inline event handlers must be call expressions... 477 | 478 | ```html 479 | 480 | ``` 481 | 482 | ...and the callee must be either a method of the component, the event, or some other whitelisted thing. It's weird, inconsistent and annoying to learn. Since there are no longer any problems around `this`, we can simplify things greatly by passing functions rather than their call expressions: 483 | 484 | ```html 485 | 486 | ``` 487 | 488 | This also allows us to do fairly sophisticated things like this: 489 | 490 | ``` 491 | 492 | ``` 493 | 494 | In Svelte 2, there is a shorthand: 495 | 496 | ```html 497 | 498 |
...
499 |
...
500 | ``` 501 | 502 | It's not yet clear if we should keep this, or replace it entirely with event bubbling, discussed [below](#events). 503 | 504 | **Custom events** in Svelte 2 enable you to use user-defined events in addition to built-in DOM events: 505 | 506 | ```html 507 |
drag me
508 | 509 | 518 | ``` 519 | 520 | This doesn't translate well to Svelte 3. But we can do something better — we can use actions instead: 521 | 522 | ```html 523 | 528 | 529 |
drag me
530 | ``` 531 | 532 | > (The 🐃 emoji used throughout this document indicates a yak that needs shaving.) 533 | 534 | The event system itself will be discussed in more detail [below](#events). 535 | 536 | 537 | ### namespace/tag options 538 | 539 | The default export from a Svelte 2 component can include compiler options — for example declaring a namespace (for SVG components) or a tag name (for custom elements): 540 | 541 | ```js 542 | export default { 543 | namespace: 'svg', 544 | tag: 'my-cool-thing' 545 | }; 546 | ``` 547 | 548 | Under this proposal, these options would be expressed by a new `` tag: 549 | 550 | ```html 551 | 552 | ``` 553 | 554 | ### Script-less components 555 | 556 | At present it is possible to create components with no ` 568 | 569 |

Hello {name}!

570 | ``` 571 | 572 | 573 | ### Events 574 | 575 | Svelte 2 components can fire events with `this.fire(eventName, optionalData)`. These events can be listened to programmatically, with `component.on(eventName, callback)`, or declaratively in component markup: 576 | 577 | ```html 578 | 579 | ``` 580 | 581 | Since there's no more `this`, there's no more `this.fire`, which means we need to rethink events. This gives us an opportunity to align with web components, where `CustomEvent` is used ([issue here](https://github.com/sveltejs/svelte/issues/1655)) — this also gives us event bubbling, which avoids needing to manually propagate events. 582 | 583 | The high-level proposal is to introduce a function called `createEventDispatcher` that would return a function for dispatching events: 584 | 585 | ```js 586 | import { createEventDispatcher } from 'svelte'; 587 | 588 | const dispatch = createEventDispatcher(); 589 | 590 | let i = 0; 591 | const interval = setInterval(() => { 592 | dispatch(i++ % 2 ? 'tick' : 'tock', { 593 | answer: 42 594 | }); 595 | }, 1000); 596 | ``` 597 | 598 | This would create a `CustomEvent` with an `event.type` of `tick` (or `tock`) and an `event.detail` of `{ answer: 42 }`. 599 | 600 | > 🐃 We could match the arguments to `new CustomEvent(name, opts)` instead — where `detail` is one of the options passed to the constructor alongside things like `bubbles` and `cancelable` 601 | 602 | > Note that `new CustomEvent` is unsupported in IE; legacy mode would need to use `document.createEvent('whatever')` instead 603 | 604 | As with lifecycle functions, the `dispatch` is bound to the component because of when `createEventDispatcher` is called. 605 | 606 | 607 | 608 | ### Component API 609 | 610 | Under this proposal, there is no longer a `this` with state and methods *inside* a component. But there needs to be a way to interact with the component from the outside world. 611 | 612 | Instantiating a top-level component remains the same, except that `data` is renamed to `props` to match the language used elsewhere: 613 | 614 | ```js 615 | import App from './App.html'; 616 | 617 | const app = new App({ 618 | target: document.querySelector('body'), 619 | props: { 620 | name: 'world' 621 | } 622 | }); 623 | ``` 624 | 625 | Exported properties can be exposed as accessors: 626 | 627 | ```js 628 | app.name; // world 629 | app.name = 'everybody'; // triggers an update synchronously 630 | ``` 631 | 632 | This creates consistent behaviour between Svelte components that are compiled to custom elements, and those that are not, while also making it easy to understand the component's contract. 633 | 634 | Of the five **built-in methods** that currently comprise the [component API](https://svelte.technology/guide#component-api) — `fire`, `get`, `set`, `on` and `destroy` — we no longer need the first two. `on` and `destroy` are still necessary, and we'll keep `set` for cases where someone needs to change multiple props at the top-level with a single update. 635 | 636 | To differentiate those built-in methods from regular properties (so that people don't need to worry about potential conflicts), `on`, `destroy` and `set` become `$on`, `$destroy` and `$set`. 637 | 638 | In some cases, a component that is designed to be used as a standalone widget will create its own **custom methods**. In Svelte 2, these are lumped in with 'private' (except not really) methods. Under this proposal, custom methods are just exported variables that happen to be functions: 639 | 640 | ```html 641 | 652 | 653 |
654 | {#if visible} 655 |

now you see me

656 | {/if} 657 |
658 | ``` 659 | 660 | ```js 661 | import Peekaboo from './Peekaboo.html'; 662 | 663 | const peekaboo = new Peekaboo(...); 664 | peekaboo.show(); 665 | ``` 666 | 667 | An `export const`, `export function` or `export class` would be a signal to the compiler that a property is read-only — in other words, attempting to assign to it would cause an error. 668 | 669 | 670 | ### `preload` and `setup` 671 | 672 | There is a rather awkward mechanism for declaring static properties on a component constructor in Svelte 2 — the `setup` hook: 673 | 674 | ```html 675 | 682 | ``` 683 | 684 | This is deeply weird, and due to an oversight (that we can't correct without a breaking change) only runs on the client. 685 | 686 | Since [Sapper](https://sapper.svelte.technology/) requires that components have some way of declaring their data dependencies prior to rendering, and since `setup` is so cumbersome, there is a special case made for `preload`. The `preload` function is attached to components on both client and server, and has no well-defined behaviour; it is purely convention. 687 | 688 | We can do better, **but it requires something potentially controversial** — a second ` 698 | 699 | 702 | 703 | {#each things as thing} 704 |

{thing}

705 | {/each} 706 | ``` 707 | 708 | `things` would be injected into the instance ` 782 | ``` 783 | 784 | That opportunity no longer exists in this RFC. Instead we need some other way to express the concept of 'all the properties that were passed into this component'. One suggestion is to use the `bind` directive on (🐃) the `` element described above: 785 | 786 | ```html 787 | 788 | 789 | 800 | 801 | 802 | 803 | ``` 804 | 805 | 806 | ### Sync vs async rendering 807 | 808 | One of the things that differentiates Svelte from other frameworks is that updates are synchronous. In other words: 809 | 810 | ```js 811 | app.set({ size: 'supersize' }); 812 | console.log(app.refs.box.offsetWidth); // already updated 813 | ``` 814 | 815 | This is easily understood, and if there is an error during rendering it is easy to isolate the source of the problem since there is usually a short stack trace to the offending `set` call. 816 | 817 | But it also has major drawbacks. It can result in the cyclical behaviour observed above, and it provides the framework no opportunity to optimise updates by (for example) batching operations that are likely to result in DOM reads (such as transition or `onupdate` callbacks) separately from the framework-initiated cycle of DOM writes. It also prevents us from implementing ideas like [time slicing](https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html). 818 | 819 | In addition, while it's possible to batch changes *within a component* (`this.set({ a, b, c })` results in a single update), changes *across* components (or those that affect stores and components together) are not batched. Now that `set` is no longer available, it is important that variable reassignments don't result in a sync update, but rather schedule an update (in a microtask). 820 | 821 | Exceptions to this rule could be made where appropriate. For example if you're tracking the height of a list... 822 | 823 | ```html 824 | 835 | 836 |

the list is {listHeight}px tall:

837 | 838 |
    839 | {#each items as item} 840 |
  • {item}
  • 841 | {/each} 842 |
843 | ``` 844 | 845 | ...then you want the change to `listHeight` to be reflected in the view as soon as all the `onupdate` callbacks have fired, *not* after an animation frame (which would result in lag). 846 | 847 | Similarly, in terms of the public component API, it might be necessary for `customElement.foo = 1` to result in a synchronous update. 848 | 849 | 850 | ### Suspense 851 | 852 | This proposal isn't directly concerned with [implementing Suspense](https://github.com/sveltejs/svelte/issues/1736), but care should be taken to ensure that Suspense can be implemented atop this proposal without breaking changes. 853 | 854 | 855 | ### TypeScript 856 | 857 | A major advantage of this proposal over Svelte 2 is that components become far more amenable to typechecking and other forms of static analysis. Even without TypeScript, VSCode is able to offer much richer feedback now than with Svelte 2. 858 | 859 | An eventual goal is to be able to use TypeScript in components: 860 | 861 | ```html 862 | 865 | 866 |

Hello {name}!

867 | ``` 868 | 869 | Editor integrations would ideally offer autocompletion and as-you-type typechecking inside the markup. This is not my area of expertise, however, so I would welcome feedback on this proposal from people who are more familiar with the TypeScript compiler API and ecosystem. 870 | 871 | 872 | ### Custom elements 873 | 874 | Svelte would continue to offer a custom element compile target. No real changes would be involved here, since this proposal brings 'vanilla' Svelte components in line with web components viz. property access and event handling. 875 | 876 | The `props` compiler option is redundant now that the contract is defined via `export`ed variables. The `tag` option can be set via `` as discussed above. 877 | 878 | 879 | ### Dependency tracking 880 | 881 | In Svelte 2, 'computed properties' use compile-time dependency tracking to derive values from state, avoiding recomputation when dependencies haven't changed. These computed properties are 'push-based' rather than 'pull-based', which can result in unnecessary work: 882 | 883 | ```html 884 | {#if visible} 885 |

{bar}

886 | {/if} 887 | 888 | 900 | ``` 901 | 902 | In the example above, there is no real need to calculate `bar` since it is not rendered, but it happens anyway. 903 | 904 | [RFC 3](https://github.com/sveltejs/rfcs/pull/8) proposed a Svelte 3 friendly alternative to computed properties that is more flexible and involves less boilerplate. 905 | 906 | ### svelte-extras 907 | 908 | Most of the methods in [svelte-extras](https://github.com/sveltejs/svelte-extras) no longer make sense. The two that we do want to reimplement are `tween` and `spring`. 909 | 910 | Happily, these will no longer involve monkey-patching components: 911 | 912 | ```html 913 | 934 | 935 | 936 | ``` 937 | 938 | 939 | ### Examples 940 | 941 | The 'imagined' components are REPL links, but they do not work in the REPL (obviously) — they're just there so it's easy to see the before/after side-by-side. 942 | 943 | * markdown editor [current](https://svelte.technology/repl?version=2.15.0&demo=binding-textarea) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=a0443aa0fc68947b5fad8fae1aa63627) 944 | * media elements [current](https://svelte.technology/repl?version=2.15.0&demo=binding-media-elements) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=8562a70e1a1e708b735b7a50e80b3cfe) 945 | * nested components [current](https://svelte.technology/repl?version=2.15.0&demo=nested-components) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=cbad006c2197633ac75fc8f31c3670df) 946 | * SVG clock [current](https://svelte.technology/repl?version=2.15.0&demo=svg-clock) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=758aacc71f506518a70c0e42f0735f6c) 947 | * Simple transition [current](https://svelte.technology/repl?version=2.15.0&demo=transitions-fade) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=7bd7b4349a27342ca96b58046eb6e765) 948 | * Custom transition [current](https://svelte.technology/repl?version=2.15.0&demo=transitions-custom) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=2e9830aad338305877d226d80a9d04d4) 949 | * Temperature converter [current](https://svelte.technology/repl?version=2.15.0&demo=7guis-temperature) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=439fc5e7c89d91a31c9c9c1447434532) 950 | 951 | 952 | ## How we teach this 953 | 954 | The success of this idea hinges on whether we can explain how **reactive assignments** work to developers who aren't compiler nerds. That means lots of examples that emphasise showing the compiled output, so that developers can understand what is happening to their code even if they don't need to care about it day-to-day. 955 | 956 | It is also essential to have plentiful examples showing how existing Svelte patterns can be implemented in this new world. 957 | 958 | 959 | ## Drawbacks 960 | 961 | Obviously, this is a breaking change. We're not the React team; we don't have the resources to support two separate paradigms. 962 | 963 | It also introduces some frankly somewhat surprising behaviour. Having spent much of the week thinking about it, and toying with existing components, I do earnestly believe that this approach feels natural once you're over the initial shock, but not everyone is guaranteed to feel that way. 964 | 965 | Overall though, this solves so many inter-related problems with Svelte that I believe the benefits to be overwhelming. 966 | 967 | 968 | ## Alternatives 969 | 970 | We toyed with a few alternative concepts: 971 | 972 | * Not doing anything, and attempting to fix the niggly edge cases within the current paradigm 973 | * Adopting something akin to React Hooks. This was met with a negative reaction from the community. Despite some minor ergonomic advantages in certain cases, the downsides of Hooks (the 'rules', the reliance on repeatedly calling user code, etc) were considered greater than the advantages 974 | * Pursuing a purer vision of reactive programming, akin to [that described by Paul Stovell](http://paulstovell.com/blog/reactive-programming). This would arguably make code more difficult to reason about, not less, and would likely introduce difficult syntactical requirements 975 | 976 | ## Unresolved questions 977 | 978 | None, I think. 979 | -------------------------------------------------------------------------------- /text/0002-reactive-stores.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2018-11-10 2 | - RFC PR: [#5](https://github.com/sveltejs/rfcs/pull/5) 3 | - Svelte Issue: (leave this empty) 4 | 5 | # Reactive stores 6 | 7 | ## Summary 8 | 9 | This RFC proposes a replacement for the existing [Store](https://svelte.technology/guide#state-management) class that satisfies the following goals: 10 | 11 | * Works with the Svelte 3 component design outlined in [RFC 1](https://github.com/sveltejs/rfcs/pull/4) 12 | * Allows any given component to subscribe to multiple sources of data from outside the component tree, rather than favouring a single app-level megastore 13 | * Typechecker friendliness 14 | * Adapts to existing state management systems like [Redux](https://redux.js.org/) or [TC39 Observables](https://github.com/tc39/proposal-observable), but does not require them 15 | * Concise syntax that eliminates lifecycle boilerplate 16 | 17 | 18 | ## Motivation 19 | 20 | In Svelte 1, we introduced the Store class, which provides a simple app-level state management solution with an API that mirrors that of individual components — `get`, `set`, `on`, `fire` and so on. The store can be attached to a component tree (or subtree), at which point it becomes available within lifecycle hooks and methods as `this.store`, and in templates by prefixing identifiers with `$`. Svelte is then able to subscribe to changes to app-level data with reasonable granularity and zero boilerplate. 21 | 22 | In Svelte 2, Store support was turned on by default. 23 | 24 | This approach works reasonably well but has some drawbacks: 25 | 26 | * It doesn't permit namespacing or nesting; the store becomes a grab-bag of values 27 | * Because of the convenience of the store being auto-attached to components, it becomes a magnet for things that aren't purely related to application state, such as effectful `login` and `logout` methods 28 | * Setting deep properties (`foo.bar.baz` etc) is cumbersome 29 | * It doesn't play well with TypeScript 30 | * Creating derived ('computed') values involves a slightly weird API 31 | * Working with existing state management systems isn't as natural as it should be 32 | 33 | A large part of the reason for the design of the original store — the fact that it auto-attaches to components, for example — is that the cost of using imported objects in Svelte 1 and 2 is unreasonably high. In Svelte 3, per [RFC 1](https://github.com/sveltejs/rfcs/pull/4), that cost will be significantly reduced. We can therefore pursue alternatives without the aforementioned drawbacks. 34 | 35 | 36 | ## Detailed design 37 | 38 | Essentially, the shift is from a single observable store of values to multiple observable values. Instead of this... 39 | 40 | ```js 41 | export default new Store({ 42 | user: { 43 | firstname: 'Fozzie', 44 | lastname: 'Bear' 45 | }, 46 | volume: 0.5 47 | }); 48 | ``` 49 | 50 | ...we create a new `writable` value store: 51 | 52 | ```js 53 | export const user = writable({ 54 | firstname: 'Fozzie', 55 | lastname: 'Bear' 56 | }); 57 | 58 | export const volume = writable(0.5); 59 | ``` 60 | 61 | Interested parties can read (and write) `user` without caring about `volume`, and vice versa: 62 | 63 | ```js 64 | import { volume } from './stores.js'; 65 | 66 | const audio = document.querySelector('audio'); 67 | 68 | const unsubscribe = volume.subscribe(value => { 69 | audio.volume = value; 70 | }); 71 | ``` 72 | 73 | Inside a component's markup, a convenient shorthand sets up the necessary subscriptions (and unsubscribes when the component is destroyed) — the `$` prefix: 74 | 75 | ```js 76 | 79 | 80 |

Hello {$user.firstname}!

81 | ``` 82 | 83 | This would compile to something like the following: 84 | 85 | ```js 86 | import { onDestroy } from 'svelte'; 87 | import { user } from './stores.js'; 88 | 89 | function init($$self, $$invalidate) { 90 | let $user; 91 | onDestroy(user.subscribe(value => { 92 | $user = value; 93 | $$invalidate('$user', $user); 94 | })); 95 | 96 | $$self.get = () => ({ $user }); 97 | } 98 | ``` 99 | 100 | 101 | ### Store API 102 | 103 | A store *must* have a `subscribe` method, and it may also have additional methods like `set` and `update` if it isn't read-only: 104 | 105 | ```js 106 | const number = writable(1); 107 | 108 | const unsubscribe = number.subscribe(value => { 109 | console.log(`value is ${value}`); // logs 1 immediately 110 | }); 111 | 112 | number.set(2); // logs 2 113 | 114 | const incr = n => n + 1; 115 | number.update(incr); // logs 3 116 | 117 | unsubscribe(); 118 | number.set(4); // does nothing — unsubscribed 119 | ``` 120 | 121 | An example implementation of this API: 122 | 123 | ```js 124 | function writable(value) { 125 | const subscribers = []; 126 | 127 | function set(newValue) { 128 | if (newValue === value) return; 129 | value = newValue; 130 | subscribers.forEach(s => s[1]()); 131 | subscribers.forEach(s => s[0](value)); 132 | } 133 | 134 | function update(fn) { 135 | set(fn(value)); 136 | } 137 | 138 | function subscribe(run, invalidate = noop) { 139 | const subscriber = [run, invalidate]; 140 | subscribers.push(subscriber); 141 | run(value); 142 | 143 | return () => { 144 | const index = subscribers.indexOf(subscriber); 145 | if (index !== -1) subscribers.splice(index, 1); 146 | }; 147 | } 148 | 149 | return { set, update, subscribe }; 150 | } 151 | ``` 152 | 153 | 154 | ### Read-only stores 155 | 156 | Some stores are read-only, created with `readable`: 157 | 158 | ```js 159 | const unsubscribe = mousePosition.subscribe(pos => { 160 | if (pos) console.log(pos.x, pos.y); 161 | }); 162 | 163 | mousePosition.set({ x: 100, y: 100 }); // Error: mousePosition.set is not a function 164 | ``` 165 | 166 | An example implementation: 167 | 168 | ```js 169 | function readable(start, value) { 170 | const subscribers = []; 171 | let stop; 172 | 173 | function set(newValue) { 174 | if (newValue === value) return; 175 | value = newValue; 176 | subscribers.forEach(s => s[1]()); 177 | subscribers.forEach(s => s[0](value)); 178 | } 179 | 180 | return { 181 | subscribe(run, invalidate = noop) { 182 | if (subscribers.length === 0) { 183 | stop = start(set); 184 | } 185 | 186 | const subscriber = [run, invalidate]; 187 | subscribers.push(subscriber); 188 | run(value); 189 | 190 | return function() { 191 | const index = subscribers.indexOf(subscriber); 192 | if (index !== -1) subscribers.splice(index, 1); 193 | 194 | if (subscribers.length === 0) { 195 | stop && stop(); 196 | stop = null; 197 | } 198 | }; 199 | } 200 | }; 201 | } 202 | 203 | const mousePosition = readable(function start(set) { 204 | function handler(event) { 205 | set({ 206 | x: event.clientX, 207 | y: event.clientY 208 | }); 209 | } 210 | 211 | document.addEventListener('mousemove', handler); 212 | return function stop() { 213 | document.removeEventListener('mousemove', handler); 214 | } 215 | }); 216 | ``` 217 | 218 | ### Derived stores 219 | 220 | A store can be derived from other stores with `derive`: 221 | 222 | ```js 223 | const a = writable(1); 224 | const b = writable(2); 225 | const c = writable(3); 226 | 227 | const total = derive([a, b, c], ([a, b, c]) => a + b + c); 228 | 229 | total.subscribe(value => { 230 | console.log(`total is ${value}`); // logs 'total is 6' 231 | }); 232 | 233 | c.set(4); // logs 'total is 7' 234 | ``` 235 | 236 | Example implementation: 237 | 238 | ```js 239 | function derive(stores, fn) { 240 | const single = !Array.isArray(stores); 241 | if (single) stores = [stores]; 242 | 243 | const auto = fn.length === 1; 244 | let value = {}; 245 | 246 | return readable(set => { 247 | let inited = false; 248 | const values = []; 249 | 250 | let pending = 0; 251 | 252 | const sync = () => { 253 | if (pending) return; 254 | const result = fn(single ? values[0] : values, set); 255 | if (auto && (value !== (value = result))) set(result); 256 | } 257 | 258 | const unsubscribers = stores.map((store, i) => store.subscribe( 259 | value => { 260 | values[i] = value; 261 | pending &= ~(1 << i); 262 | if (inited) sync(); 263 | }, 264 | () => { 265 | pending |= (1 << i); 266 | }) 267 | ); 268 | 269 | inited = true; 270 | sync(); 271 | 272 | return function stop() { 273 | run_all(unsubscribers); 274 | }; 275 | }); 276 | } 277 | ``` 278 | 279 | > In the example above, `total` is recalculated immediately whenever the values of `a`, `b` or `c` are set. In some situations that's undesirable; you want to be able to set `a`, `b` *and* `c` without `total` being recalculated until you've finished. That could be done by putting the `set(fn(...values))` in a microtask, but that has drawbacks too. (Of course, that could be a decision left to the user.) Is this a fatal flaw in the design — should we strive for pull-based rather than push-based derived values? Or is it fine in reality? 280 | 281 | Derived stores are, by nature, also read-only. They could be used, for example, to filter the items in a todo list: 282 | 283 | ```html 284 | 294 | 295 | 299 | 300 | {#each $filtered as todo} 301 |

{todo.description}

302 | {/each} 303 | ``` 304 | 305 | 306 | ### Relationship with TC39 Observables 307 | 308 | There is a [stage 1 proposal for an Observable object](https://github.com/tc39/proposal-observable) in JavaScript itself. 309 | 310 | Cards on the table: I'm not personally a fan of Observables. I've found them to be confusing and awkward to work with. But there are particular reasons why I don't think they're a good general solution for representing reactive values in a component: 311 | 312 | * They don't represent a single value changing over time, but rather a stream of distinct values. This is a subtle but important distinction 313 | * Two different subscribers to the same Observable could receive different values (!), where as in a UI you want two references to the same value to be guaranteed to be consistent 314 | * Observables can 'complete', but declarative components (in Svelte and other frameworks) deliberately do not have a concept of time. The two things are incompatible 315 | * They have error-handling semantics that are very often redundant (what error could occur when observing the mouse position, for example?). When they're not redundant (e.g. in the case of data coming over the network), errors are perhaps best handled out-of-band, since the goal is to concisely represent the value in a component template 316 | 317 | Of course, some Observables *are* suitable for representing reactive values in a template, and they could easily be adapted to work with this design: 318 | 319 | ```js 320 | function adaptor(observable) { 321 | return { 322 | subscribe(fn) { 323 | const subscriber = observable.subscribe({ 324 | next: fn 325 | }); 326 | 327 | return subscriber.unsubscribe; 328 | } 329 | } 330 | } 331 | 332 | const observable = Observable.of('red', 'green', 'blue'); 333 | const store = adaptor(observable); 334 | 335 | const unsubscribe = store.subscribe(color => { 336 | console.log(color); // logs red, then green, then blue 337 | }); 338 | ``` 339 | 340 | 341 | ### Examples of use with existing state management libraries 342 | 343 | More broadly, the same technique will work with existing state management libraries, as long as they expose the necessary hooks for observing changes. (I've found this to be difficult with MobX, but perhaps I'm just not sufficiently familiar with that library — would welcome submissions.) 344 | 345 | 346 | #### Redux 347 | 348 | ```js 349 | // src/redux.js 350 | import { createStore } from 'redux'; 351 | 352 | export const reduxStore = createStore((state = 0, action) => { 353 | switch (action.type) { 354 | case 'INCREMENT': 355 | return state + 1 356 | case 'DECREMENT': 357 | return state - 1 358 | default: 359 | return state 360 | } 361 | }); 362 | 363 | function adaptor(reduxStore) { 364 | return { 365 | subscribe(fn) { 366 | return reduxStore.subscribe(() => { 367 | fn(reduxStore.getState()); 368 | }); 369 | } 370 | }; 371 | } 372 | 373 | export const store = adaptor(reduxStore); 374 | ``` 375 | 376 | ```html 377 | 378 | 381 | 382 | 385 | ``` 386 | 387 | 388 | #### Immer 389 | 390 | ```js 391 | import { writable } from 'svelte/store.js'; 392 | import { produce } from 'immer'; 393 | 394 | function immerObservable(data) { 395 | const store = writable(data); 396 | 397 | function update(fn) { 398 | store.update(state => produce(state, fn)); 399 | } 400 | 401 | return { 402 | update, 403 | subscribe: store.subscribe 404 | }; 405 | } 406 | 407 | const todos = immerObservable([ 408 | { done: false, description: 'walk the dog' }, 409 | { done: false, description: 'mow the lawn' }, 410 | { done: false, description: 'do the laundry' } 411 | ]); 412 | 413 | todos.update(draft => { 414 | draft[0].done = true; 415 | }); 416 | ``` 417 | 418 | 419 | #### Shiz 420 | 421 | ```js 422 | import { readable } from 'svelte/store.js'; 423 | import { value, computed } from 'shiz'; 424 | 425 | const a = value(1); 426 | const b = computed([a], ([a]) => a * 2); 427 | 428 | function shizObservable(shiz) { 429 | return readable(function start(set) { 430 | return shiz.on('change', () => { 431 | set(shiz.get()); 432 | }); 433 | }, shiz.get()); 434 | } 435 | 436 | const store = shizObservable(b); 437 | 438 | const unsubscribe = store.subscribe(value => { 439 | console.log(value); // logs 2 440 | }); 441 | 442 | a.set(2); // logs 4 443 | ``` 444 | 445 | 446 | ### Using with Sapper 447 | 448 | At present, `Store` gets privileged treatment in [Sapper](https://sapper.svelte.technology) apps. A store instance can be created per-request, for example to contain user data. This store is attached to the component tree at render time, allowing `{$user.name}` to be server-rendered; its data is then passed to a client-side store. 449 | 450 | It's essential that this functionality be preserved. I'm not yet sure of the best way to achieve that. The most promising suggestion is that we use regular props instead, passed into the top-level component. (In some ways this would be more ergonomic, since the user would no longer be responsible for setting up the store client-side.) 451 | 452 | > Potential corner-cases to discuss: 453 | > * What happens if a subscriber causes another subscriber to be removed (e.g. it results in a component subtree being destroyed)? 454 | > * Is `$user.name` ambiguous (i.e. is `user` the store, or `user.name`?) and if so how do we resolve the ambiguity 455 | > * What happens if `$user` is declared in a scope that has a store `user`? Do we just not subscribe? 456 | 457 | 458 | ## How we teach this 459 | 460 | As with RFC 1, it's crucial that this be introduced with ample demos of how the `$` prefix works, in terms of the generated code. 461 | 462 | It's arguably simpler to teach than the existing store, since it's purely concerned with data, and avoids the 'magic' of auto-attaching. 463 | 464 | 465 | ## Drawbacks 466 | 467 | Like RFC 1, this is a breaking change, though RFC 1 will break existing stores anyway. The main reason not to pursue this option would be that the `$` prefix is overly magical, though I believe the convenience outweighs the modest learning curve. 468 | 469 | Another potential drawback is that anything that uses a store (except the markup) must *itself* become a reactive store; they are [red functions](http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). But this problem is presumably fundamental, rather than an avoidable consequence of the approach we've happened to choose. 470 | 471 | > [RFC 3](https://github.com/sveltejs/rfcs/blob/reactive-declarations/text/0003-reactive-declarations.md) presents an escape hatch to this problem 472 | 473 | 474 | ## Alternatives 475 | 476 | * The existing store (but only at the top level; there is no opportunity to add stores at a lower level in the new design) 477 | * Baking in an existing state management library (and its opinions) 478 | * Using TC39 Observables 479 | * Not having any kind of first-class treatment of reactive values, and relying on the reactive assignments mechanism exclusively 480 | 481 | 482 | ## Unresolved questions 483 | 484 | * The Sapper question 485 | * The exact mechanics of how typechecking would work 486 | -------------------------------------------------------------------------------- /text/0003-reactive-declarations.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2018-11-28 2 | - RFC PR: [#8](https://github.com/sveltejs/rfcs/pull/8) 3 | - Svelte Issue: (leave this empty) 4 | 5 | # Reactive declarations 6 | 7 | ## Summary 8 | 9 | This RFC proposes an implementation of the 'destiny operator' inside Svelte components, using a little-known and rarely-used JavaScript feature called [labels](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label): 10 | 11 | ```html 12 | 20 | 21 |

Twice {count} is {doubled}; twice that is {quadrupled}

22 | 23 | 24 | ``` 25 | 26 | 27 | ## Motivation 28 | 29 | In [RFC 1](https://github.com/sveltejs/rfcs/blob/reactive-assignments/text/0001-reactive-assignments.md), we introduced the idea of *reactive assignments*, in which assigning to a component's local variables... 30 | 31 | ```js 32 | count += 1; 33 | ``` 34 | 35 | ...causes the component to update. 36 | 37 | As an indirect consequence of that change, we're able to get rid of all the boilerplate associated with Svelte 2 components, greatly simplifying both the compiler and the user's application code. But it also means getting rid of [computed properties](https://svelte.technology/guide#computed-properties), which are a powerful and convenient mechanism for push-based reactivity: 38 | 39 | ```html 40 |

Twice {count} is {doubled}; twice that is {quadrupled}

41 | 42 | 43 | 44 | 56 | ``` 57 | 58 | The useful thing about computed properties is that they are only recalculated when their inputs change, side-stepping a common performance problem that affects some frameworks in which derived values must be recalculated on every render. Unlike some other frameworks that implement computed properties, the compiler is able to build a dependency graph of computed properties at *compile time*, enabling it to sort them topologically and generate highly efficient code that doesn't depend on expensive *run time* dependency tracking. 59 | 60 | RFC 1 initially glossed over the loss of computed properties, suggesting that we could simply replace them with functions: 61 | 62 | ```html 63 | 68 | 69 |

Twice {count} is {doubled()}; twice that is {quadrupled()}

70 | 71 | 72 | ``` 73 | 74 | But in this scenario, `doubled` must be called twice (once directly, once via `quadrupled`). Not only that, but we have to trace which values are read when `doubled` and `quadrupled` are called so that we know when we need to update them; in some cases it would be necessary to bail out of that optimisation and call those functions whenever *anything* changed. In more realistic examples, this results in a lot of extra work relative to Svelte 2. 75 | 76 | The only way to avoid that work is with a [Sufficiently Smart Compiler](http://wiki.c2.com/?SufficientlySmartCompiler). We need an alternative. 77 | 78 | 79 | ## Detailed design 80 | 81 | In [What is Reactive Programming?](http://paulstovell.com/blog/reactive-programming), Paul Stovell introduces the 'destiny operator': 82 | 83 | ``` 84 | var a = 10; 85 | var b <= a + 1; 86 | a = 20; 87 | Assert.AreEqual(21, b); 88 | ``` 89 | 90 | If we could use the destiny operator in components, the Svelte 3 compiler would be able to do exactly what it does with Svelte 2 computed properties — update `b` whenever `a` changes. 91 | 92 | Unfortunately we can't, because that would be invalid JavaScript, and it's important for many reasons that everything inside a component's ` 210 | 211 |

Hello {$user.name}!

212 | 213 | {#each $todos as todo} 214 |

{todo.description}

215 | {/each} 216 | ``` 217 | 218 | One limitation of reactive stores is that it's difficult to mix them with a component's local state. For example, if we wanted a filtered view of those todos, we can't simply derive a new store that uses a local filter variable — 219 | 220 | ```html 221 | 228 | 229 |

Hello {$user.name}!

230 | 231 | 235 | 236 | {#each $filtered as todo} 237 |

{todo.description}

238 | {/each} 239 | ``` 240 | 241 | — because `filtered` doesn't have a way to know when `hideDone` changes. Instead, we'd need to create a new store: 242 | 243 | ```diff 244 | 254 | 255 |

Hello {$user.name}!

256 | 257 | 262 | 263 | {#each $filtered as todo} 264 |

{todo.description}

265 | {/each} 266 | ``` 267 | 268 | Reactive declarations offer an alternative, if we allow the same treatment of values with the `$` prefix: 269 | 270 | ```diff 271 | 280 | 281 |

Hello {$user.name}!

282 | 283 | 287 | 288 | -{#each $filtered as todo} 289 | +{#each filtered as todo} 290 |

{todo.description}

291 | {/each} 292 | ``` 293 | 294 | The obvious problem with this is that `$todos` isn't defined anywhere in the ` 87 | 88 | 89 | ``` 90 | 91 | ```html 92 | 93 |
94 | {number} 95 | {name} 96 |
97 | ``` 98 | 99 | 100 | #### Better approach? 101 | 102 | ```html 103 | 115 | 116 | 117 |
118 | 119 |
120 |
121 | ``` 122 | 123 | 124 | ## Detailed design 125 | 126 | ### Lazy rendering 127 | 128 | For much of what's outlined above, we need slotted content's lifecycle to be controlled by the child component, rather than the parent. This is at odds with the current approach, in which all slotted content is rendered by the parent and then passed to the child, much like light DOM is passed to a custom element. 129 | 130 | I'll update this section with some example code as soon as I can unknot my brain. 131 | 132 | 133 | ### Uncontrolled component context 134 | 135 | For a pattern like this to work... 136 | 137 | ```html 138 | 139 | 140 | Title 1 141 | Title 2 142 | 143 | 144 | 145 |

Any content 1

146 |
147 | 148 |

Any content 2

149 |
150 |
151 | ``` 152 | 153 | ...each `` component needs to be able to tell `` (possibly via ``, depending on how the component is authored) that it has been selected. It also needs to know its index relative to its siblings, even if it is created after the initial render, or an earlier tab is removed after the initial render. 154 | 155 | Similarly, each `` needs to know its own index (which is again subject to mutations) and the currently selected index. If a panel *is* selected, it should render its child content, otherwise it should not. 156 | 157 | Perhaps it could look like this: 158 | 159 | ```html 160 | 161 | 164 | 165 | 203 | 204 |
205 | 206 |
207 | ``` 208 | 209 | ```html 210 | 211 | 224 | 225 | 228 | ``` 229 | 230 | ```html 231 | 232 | 244 | 245 | {#if $selected === panel} 246 | 247 | {/if} 248 | ``` 249 | 250 | > This is just a first draft — I'm glossing over a few things in this example 251 | 252 | Here, `setContext` and `getContext` are functions that must be called during the component's initialisation, like lifecycle functions. `getContext(arg)` retrieves any context that was set using `arg` (which could be anything including a string like 'tabs', but using `{}` guarantees no collisions) *in a parent component*. This allows any given component to have multiple instances of ``, or even for `` to be nested. 253 | 254 | > TODO draw the rest of the owl 255 | 256 | 257 | ### Explicit slot scope 258 | 259 | The above example works for *implicit* context, i.e. that shared between components without the app author having to worry about it. Sometimes, we need *explicit* context, or 'scope', such as with the `` example. 260 | 261 | React can achieve this with the render prop pattern: 262 | 263 | ```html 264 | {({ number, name }) => 265 |
266 | {number} 267 | {name} 268 |
269 | }
270 | ``` 271 | 272 | That's a little trickier for us. Perhaps we could achieve the same result with a new directive, `let`: 273 | 274 | ```html 275 | 276 |
277 | {item.number} 278 | {item.name} 279 |
280 |
281 | ``` 282 | 283 | The `let:item` directive, short for `let:item={item}`, makes `item` available to child content, in much the same way as `{#each items as item}`. The `` component would make it available like so: 284 | 285 | ```html 286 | {#each visible as row (row.index)} 287 |
288 | 289 |
290 | {/each} 291 | ``` 292 | 293 | We could potentially allow destructuring as well: 294 | 295 | ```html 296 | 297 |
298 | {number} 299 | {name} 300 |
301 |
302 | ``` 303 | 304 | For non-default slots, the directive would live on the slotted element: 305 | 306 | ```html 307 |
308 |

Copyright {year} SvelteJS Inc

309 |
310 | ``` 311 | 312 | In terms of the generated code, the child component would augment its own context with any properties on the slot: 313 | 314 | ```js 315 | const slot_scope = Object.assign(component_ctx, { 316 | item: ctx.item 317 | }); 318 | ``` 319 | 320 | > TODO draw the rest of the owl 321 | 322 | 323 | ## How we teach this 324 | 325 | Terminology-wise, 'uncontrolled component' and 'context' are terms in fairly widespread use in the React community, and 'slot scope' is used in Vue-land. It makes sense to use the same language. 326 | 327 | For the vast majority of users, these changes are purely additive, requiring no real reorganization of existing documentation. The only change (see 'drawbacks') is that `` no longer behaves exactly like its native HTML equivalent, since we're now able to inject slot scope. 328 | 329 | 330 | ## Drawbacks 331 | 332 | It's tricky to implement, and adds complexity. Then again the current slot mechanism is also somewhat tricky. 333 | 334 | The current method makes it possible to use the slots API programmatically, by passing a `slot: { default, named }` object at initialisation (where `default` and `named` are HTML elements or document fragments). That would no longer be possible if the lifecycle is controlled by the child rather than the parent. Personally I haven't ever used this API, and I'd be surprised if many people have, but it is a drawback nonetheless. 335 | 336 | Finally, this proposal moves us away from alignment with web components. A Svelte component that had `` inside an each block (such as svelte-virtual-list) couldn't realistically be compiled to a web component. It wouldn't be much use as a web component in its current form though. 337 | 338 | ## Alternatives 339 | 340 | The alternative is to do nothing, and rely on existing methods of composition. As we've seen, there are limits to the current approach. 341 | 342 | Context could be designed differently. React does it like this: 343 | 344 | ```js 345 | import { createContext, useContext } from 'react'; 346 | 347 | const ThemeContext = createContext('light'); 348 | 349 | function App() { 350 | return ( 351 | 352 | 353 | 354 | ); 355 | } 356 | 357 | function ChildComponent() { 358 | const theme = useContext(ThemeContext); 359 | 360 | return ( 361 |
362 |

Current theme is {theme}

363 |
364 | ); 365 | } 366 | ``` 367 | 368 | In other words, context can be created anywhere inside the component's markup, rather than just in the ` 45 | 46 | e.detail.toUpperCase()} let:aSlot={aNumber} /> 47 | ``` 48 | 49 | The props/events/slots are typed and other props/events/slots would throw type errors. 50 | 51 | ### Implementation: 52 | 53 | Inside `language-tools`, we already have such a type definition which is used internally and looks like this: 54 | 55 | ```ts 56 | class Svelte2TsxComponent< 57 | Props extends Record = any, 58 | Events extends Record = any, 59 | Slots extends Record = any 60 | > { 61 | // The following three exist for type checking capabilities only 62 | // and do not exist at runtime: 63 | $$prop_def: Props; 64 | $$events_def: Events; 65 | $$slot_def: Slots; 66 | 67 | constructor(options: { 68 | // ... 69 | props?: Props & Record; // <-- typed as Props, Record for the $$restProps possibility 70 | }); 71 | 72 | $on( 73 | event: K, 74 | handler: (e: Events[K]) => void 75 | ): () => void; // <-- typed 76 | $set(props: Partial & Record): void; // <-- typed, Record for the $$restProps possibility 77 | // ... 78 | } 79 | ``` 80 | 81 | The Svelte component definition is enhanced with typings for Props, Events, Slots. Enhancing the types for `$on`/`$set` and the `props` constructor param is straightforward. The `$$prop_def`/`$$events_def`/`$$slot_def` properties exist purely for type checking: They are used by the language server to provide intellisense. For the intellisense to extract the correct types, they need to be defined on the component instance. Furthermore, `$$prop_def` is a must because we transform HTMLx to JSX/TSX for the intellisense, and for that we need to provide a property for input props (`$$prop_def`). 82 | 83 | Obviously it's not ideal to have type-only-properties on the definition, but with a big enough disclaimer/comment on it, this should be okay in my opinion, especially since the `$$`-prefix is already the "internal, don't use this"-notion for other properties/methods. 84 | 85 | Regarding "where to put this": This is up to discussion. Currently I think enhancing `SvelteComponentDev` (which is `SvelteComponent` on the outside) with that definition is the best option. This would not be a breaking change to the existing definition because of the `any` type by default for props/events/slots. 86 | 87 | ## How we teach this 88 | 89 | Docs on the official site as well as in the `language-tools` docs. Using it should feel straightforward for anyone who has worked with TS / library definitions before. 90 | 91 | ## Drawbacks 92 | 93 | The `$$prop_def`/`$$events_def`/`$$slot_def` leak. 94 | 95 | ## Alternatives 96 | 97 | We need to provide these type definitions for a clear path forward, so the only alternatives are _where_ to put them. 98 | 99 | - One would be a dedicated package. But I think this feels odd/inconsistent from a developer perspective. 100 | - Another alternative would be to provide this as part of `"svelte"`, but as its own class definition (name could be `SvelteComponentDefinition`). Feels slightly less odd/inconsistent than the first option. 101 | 102 | ## Unresolved questions 103 | 104 | - Anything that can be fine-tuned on the proposed class type definition? 105 | - Anything that I forgot that is no longer possible/throws a type error when the types are more strict? 106 | -------------------------------------------------------------------------------- /text/0006-style-properties.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2019-11-12 2 | - RFC PR: [#13](https://github.com/sveltejs/rfcs/pull/13) 3 | - Svelte Issue: [#5628](https://github.com/sveltejs/svelte/issues/5628) 4 | 5 | # Passing CSS custom properties to components 6 | 7 | 8 | ## Summary 9 | 10 | This RFC proposes an idiomatic way to pass styles to components for the purposes of theming, using CSS custom properties: 11 | 12 | ```html 13 | 20 | ``` 21 | 22 | 23 | ## Motivation 24 | 25 | Theming components is currently too difficult. Suppose you were using the `` component from the (fictional) Potato Design System. Perhaps it has markup like this: 26 | 27 | ```html 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ``` 36 | 37 | (For brevity I'm omitting `tabindex`, `role` and various `aria-*` attributes that a real slider component would include for a11y reasons.) 38 | 39 | Suppose further that we want to control (say) the colour of the rail and the track, or the size of the thumb. At present, there are only bad options: 40 | 41 | 42 | ### Global CSS 43 | 44 | Because Svelte deliberately leaves class names intact, it's possible to select `.potato-slider-rail` etc from any stylesheet. But while this can be a useful escape hatch, it's not something to encourage as a method for theming, since it breaks encapsulation (the component has no control over which aspects of its styles can be externally controlled, even while it has complete control over which values are props and which are internal state) and is likely to lead to broken or buggy styles. 45 | 46 | It's also brittle, since it treats internal (potentially changeable) implementation details as a public API. A good theming solution is explicit about which style properties can be changed externally, just as components are explicit about which props are accessible to consumers. 47 | 48 | Furthermore, it becomes more difficult to style components differently based on where they appear in an app. 49 | 50 | 51 | ### The `:global` modifier 52 | 53 | A consuming component can target the same internal class names (or any other selector) using `:global(...)`, optionally inside a non-global selector to restrict the styles to children of that component. 54 | 55 | This has all of the downsides of global CSS (except being able to style different instances of a component differently) plus more: it may result in the need for additional DOM, and... it's kinda ugly. It feels like a hack. 56 | 57 | 58 | ### Props 59 | 60 | An alternative approach is for the component to expose props that are then used for styling: 61 | 62 | ```html 63 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ``` 77 | 78 | There's a few things not to like about this. Firstly, we're conflating state and styles. Secondly, we have to overuse inline styles to apply those values, and the compiler has to generate extra code to make that possible. 79 | 80 | Thirdly, this provides no good way to control theming at an app level. Every time `` is used, its style properties must be repeated. In most cases, that's probably not what we want. 81 | 82 | 83 | ## Detailed design 84 | 85 | 86 | **Note: a previous implementation proposal worked by passing styles down as `ctx.$$styles`. This would have worked, but introduced some modest but unfortunate overhead. [It's preserved here](https://gist.github.com/Rich-Harris/6ee465dca7e86e5743cb367ba0ae3bee).** 87 | 88 | Style properties — handily distinguishable from regular properties by the leading `--` used by CSS custom properties — are essentially syntactic sugar for a wrapper element. The example at the top of this document... 89 | 90 | ```html 91 | 98 | ``` 99 | 100 | ...desugars to something like this: 101 | 102 | ```html 103 |
104 | 109 |
110 | ``` 111 | 112 | `display: contents` essentially removes the wrapper element from the DOM, but allows it to set inheritable styles including custom properties. [It's easier to show than tell](https://svelte.dev/repl/ea454b5d951141ce989bf9ce46767c71?version=3.14.0). It's supported in [all modern browsers](https://caniuse.com/#feat=css-display-contents) (ignore notes 2 and 3, they don't apply in this situation), including Edge when the Chromium version ships. 113 | 114 | In unsupported browsers, there is a chance it would break some layouts (though setting `width: 100%; height: 100%` would fix many of those). Those browsers generally don't support custom properties anyway, so this should probably be considered a modern-browser-only feature. 115 | 116 | 🐃 It *would* be possible for someone to accidentally target the `
`, so it may be preferable to use a made-up element name instead (``?). In SVG, there might not be any choice but to use a ``. In 99.9% of cases it wouldn't matter in the least, but it *would* need to be documented. 117 | 118 | 119 | ### Global theming 120 | 121 | Because this approach uses custom properties, it becomes straightforward to set theme styles globally, or for a large subtree of the app: 122 | 123 | ```css 124 | /* global.css */ 125 | html { 126 | --rail-color: black; 127 | --track-color: red; 128 | } 129 | ``` 130 | 131 | ```html 132 | 133 |
134 | 135 |
136 | 137 | 143 | ``` 144 | 145 | A consumer of the component can override them... 146 | 147 | ```html 148 | 149 | ``` 150 | 151 | ...but ultimately the component itself has control over what is exposed, and can specify its own fallback values using normal CSS custom property syntax: 152 | 153 | ```html 154 | 155 | 160 | ``` 161 | 162 | 163 | ## How we teach this 164 | 165 | This ought to be straightforward to teach, at least to people already familiar with custom properties. Terminology-wise, it probably makes sense to refer to 'style props' to distinguish them from regular component props. 166 | 167 | It would require a new tutorial chapter and updated documentation. 168 | 169 | 170 | ## Drawbacks 171 | 172 | *Technically* this would be a breaking change, since `--foo` is currently passed down as a regular prop (albeit only accessible via `$$props['--foo']`). I feel pretty confident saying this isn't a real world concern. 173 | 174 | There are some more salient drawbacks: 175 | 176 | * It relies on something that is inherently global. Different components might 'claim' a given property name. While it's possible to differentiate them at the subtree level, it's not possible to do so globally. 177 | * IE11 doesn't support custom properties or `display: contents`, so we'd be pushing the ecosystem towards incompatibility with that browser. Should we care? Probably not. Component authors who wanted to support IE11 would have to provide fallback values, and consumers of those components would have to be okay with those fallbacks. 178 | * Regular component properties are statically analysable, which could one day allow us to have typechecking and autocompletion when using those components. The same isn't true for style properties. We could imagine some way of changing that (some syntax that lives inside, or on, the ` 232 | ``` 233 | 234 | Aside from being an implementation nightmare, I think the proposal in this RFC is *strictly better* than props-in-style — it gives you the same expressive power in a neater, more idiomatic way, along with the global theming ability. 235 | 236 | 237 | ## Unresolved questions 238 | 239 | I'm not a big design system user, so I would very much like to get feedback from people who are. Would this solve your problems? What have we missed? 240 | 241 | In particular, this takes a different approach from [CSS Shadow Parts](https://drafts.csswg.org/css-shadow-parts-1/), which allows a component consumer to target selected elements, but to then apply arbitrary styles to those elements. I'm personally surprised about that, given the degree to which web component advocates prioritise encapsulation — it seems like a footgun, honestly — but I'd be eager to learn from people with relevant experience. (Note that we'd still be able to emulate that capability with `:global` — really the question is whether it needs to be first-class.) 242 | -------------------------------------------------------------------------------- /text/0007-markup-constants.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2020-09-09 2 | - RFC PR: [#33](https://github.com/sveltejs/rfcs/pull/33) 3 | - Svelte Issue: (leave this empty) 4 | 5 | # Constants in markup 6 | 7 | ## Summary 8 | 9 | Add a new `{@const ...}` tag that defines a local constant. 10 | 11 | ## Motivation 12 | 13 | Consider a component with an `each` block: 14 | 15 | ```svelte 16 | 19 | 20 | {#each boxes as box} 21 |
25 | {box.width} * {box.height} = {box.width * box.height} 26 |
27 | {/each} 28 | ``` 29 | 30 | Suppose we'd like to add a `large` class for boxes over 10,000 square pixels. We could do it like this... 31 | 32 | ```svelte 33 | 36 | 37 | {#each boxes as box} 38 |
= 10000} 41 | style="width: {box.width}px; height: {box.height}px" 42 | > 43 | {box.width} * {box.height} = {box.width * box.height} 44 |
45 | {/each} 46 | ``` 47 | 48 | ...but that duplicates the `box.width * box.height` expression. If we were to add `medium` and `jumbo` classes, this would quickly get out of hand. We could define a helper function instead... 49 | 50 | ```svelte 51 | 56 | 57 | {#each boxes as box} 58 |
= 10000} 61 | style="width: {box.width}px; height: {box.height}px" 62 | > 63 | {box.width} * {box.height} = {area(box)} 64 |
65 | {/each} 66 | ``` 67 | 68 | ...but adding logic to the ` 81 | 82 | {#each boxes_with_area as box} 83 |
= 10000} 86 | style="width: {box.width}px; height: {box.height}px" 87 | > 88 | {box.width} * {box.height} = {box.area} 89 |
90 | {/each} 91 | ``` 92 | 93 | ...but that feels like a hack. 94 | 95 | These situations crop up from time to time, and the various workarounds are labour-intensive, involve wasted computation, and impede readability and refactoring. 96 | 97 | ## Detailed design 98 | 99 | Suppose we added a new `{@const ...}` tag to the Svelte template language. We could define `area` where it is used: 100 | 101 | ```svelte 102 | 105 | 106 | {#each boxes as box} 107 | {@const area = box.width * box.height} 108 | 109 |
= 10000} 112 | style="width: {box.width}px; height: {box.height}px" 113 | > 114 | {box.width} * {box.height} = {area} 115 |
116 | {/each} 117 | ``` 118 | 119 | The `@const` indicates that the value is read-only (i.e. it cannot be assigned to or mutated in an expression such as an event handler), and communicates, through its similarity to `const` in JavaScript, that it only applies to the current scope (i.e. the current block or element). 120 | 121 | Attempting to read a constant outside its scope would be a reference error (unless the constant was already shadowing a value, of course): 122 | 123 | ```svelte 124 | {#each boxes as box} 125 | {@const area = box.width * box.height} 126 | 127 | 128 | {/each} 129 | 130 |

this is a reference error: {area}

131 | ``` 132 | 133 | ### Hoisting and TDZ 134 | 135 | With JavaScript `const` (and `let`), values cannot be read before they have been initialised. We could choose to do the same thing here, though in our case it is an unnecessary restriction — we could very simply hoist values to the top of their block, in the order in which they're declared: 136 | 137 | ```svelte 138 | {#if n} 139 |

{n}^4 = {hypercubed}

140 | 141 | {@const squared = n * n} 142 | {@const cubed = squared * n} 143 | {@const hypercubed = cubed * n} 144 | {/if} 145 | ``` 146 | 147 | > 🐃 You could argue that this improves or degrades readability depending on your perspective. You could also argue in favour of topological ordering, as we have in the case of reactive statements/declarations. I don't yet have strong opinions one way or the other. 148 | 149 | ### Conflicts 150 | 151 | Defining the same constant twice in a single block would be a compile error: 152 | 153 | ```svelte 154 | {@const foo = a} 155 | {@const foo = b} 156 | ``` 157 | 158 | 159 | ## How we teach this 160 | 161 | I've provisionally suggested `{@const ...}` for the reasons articulated above, namely the similarities to `const`, so this new feature could be called a 'const tag'. The keyword is open to bikeshedding though — perhaps someone could make a case for calling it `{@value ...}` or `{@local ...}` instead. 162 | 163 | Accepting this proposal wouldn't require any changes to the existing docs, but would require some new documentation to be created. 164 | 165 | ## Drawbacks 166 | 167 | The evergreen answer to the 'why should be *not* do this?' question is 'it increases the amount of stuff to learn, and is another thing that we have to implement and maintain'. 168 | 169 | In this case though there's an additional reason we might consider not adding this. One of the arguments that's frequently deployed in favour of JSX-based frameworks over template-based ones is that JSX allows you to use existing language constructs: 170 | 171 | ```js 172 | {boxes.map(box => { 173 | const area = box.width * box.height; 174 | 175 | return
= 10000} 178 | style="width: {box.width}px; height: {box.height}px" 179 | > 180 | {box.width} * {box.height} = {area} 181 |
182 | })} 183 | ``` 184 | 185 | The complaint is that by choosing less powerful languages, template-based frameworks are then forced to reintroduce uncanny-valley versions of those constructs in order to add back in missing functionality, thereby increasing the mount of stuff people have to learn. 186 | 187 | In general, I'm unpersuaded by these arguments (learning curve is determined not just by unfamiliar syntax, but by unfamiliar semantics and APIs as well, and the frameworks in question excel at adding complexity in those areas). But this *is* a case where it feels like we're papering over a deficiency in our language, and is the sort of thing detractors might well point to and say 'ha! see?'. Whether or not that's a reason not to pursue this RFC is a matter for collective judgment. 188 | 189 | ## Alternatives 190 | 191 | The alternatives are shown above in the 'motivation' section; they are not good. 192 | 193 | ## Unresolved questions 194 | 195 | * Is `@const` the best keyword? 196 | * Should we allow multiple declarations per tag? (Probably not.) 197 | * Does TDZ apply? 198 | * Are declarations ordered as-authored, or topologically? -------------------------------------------------------------------------------- /text/0008-style-directives.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2020-12-09 2 | - RFC PR: [#42](https://github.com/sveltejs/rfcs/pull/42) 3 | - Svelte Issue: (leave this empty) 4 | 5 | # Style Directive 6 | 7 | ## Summary 8 | 9 | Add a `style:` directive: 10 | 11 | ```svelte 12 |
13 | ``` 14 | 15 | ## Motivation 16 | 17 | Currently, when inline style attributes need to be set based on multiple variables, the construction of a style attribute string is left to the developer. 18 | 19 | Constructing these attribute strings can become somewhat clumsy. 20 | 21 | For example: 22 | 23 | ```svelte 24 |
31 | ``` 32 | 33 | It would be useful — and often less error-prone — to be able to set multiple inline styles without having to construct the attribute string: 34 | 35 | ```svelte 36 |
41 | ``` 42 | 43 | ## Detailed design 44 | 45 | Style directives would take the form `style:property="value"` where `property` is a CSS property and `value` is a CSS property value. 46 | 47 | ### Shorthand 48 | 49 | A shorthand form could also exist when a variable name and CSS property name match: 50 | 51 | ```svelte 52 | 55 |
56 | ``` 57 | 58 | ### Conflicting Styles 59 | 60 | One question surrounding this approach how to combine style directives with style attribures that have conflicting properties. 61 | 62 | Consider the example: 63 | 64 | ```svelte 65 |
66 | ``` 67 | 68 | What would be the value of this element's `left` property? 69 | 70 | It seems there are three options available: 71 | 72 | 1. Conflicting CSS properties in style directives are not allowed, and a compiler error is thrown. 73 | 2. The style attribute takes precedence. 74 | 3. The style directive takes precedence. 75 | 76 | The third option seems like the most reasonable, and most closely matches the behavior of the `class:` directive. 77 | 78 | For example, class directives take precedence over class attribute strings: 79 | 80 | ```svelte 81 |
82 | My class is "foo" and not "foo bar" 83 |
84 | ``` 85 | 86 | ### CSS Variables 87 | 88 | These directives could also contain CSS variables: 89 | 90 | ```svelte 91 |
92 | ``` 93 | 94 | ## How we teach this 95 | 96 | This is pretty similar to the `class:` directive, except that a CSS value is used instead of a boolean. 97 | 98 | ## Drawbacks 99 | 100 | As with any feature, this would add some complexity to the codebase, and add a bit of learning curve, although the impact seems minimal. 101 | 102 | We may want to discourage the construction of complex inline styles based on variables, and encourage the use of CSS variables instead. 103 | Of course, dynamic CSS variables would also need to be passed into a style attribute. 104 | 105 | ## Alternatives 106 | 107 | React uses a style object: 108 | 109 | ```jsx 110 |
111 | ``` 112 | 113 | We could instead mimic React and allow this sort of object to be passed to an element's `style` attribute. 114 | 115 | ## Unresolved questions 116 | 117 | ### Camel Case Property Names? 118 | 119 | We could also allow for the use of camel case property names. 120 | 121 | This would make using variable names that match CSS properties a bit easier: 122 | 123 | ```svelte 124 | 127 |
128 | ``` 129 | 130 | --------------------------------------------------------------------------------