mirror of https://github.com/sveltejs/svelte
parent
0770b54519
commit
29052aba7d
@ -1,46 +1,45 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": [ 2, "tab", { "SwitchCase": 1 } ],
|
"indent": [2, "tab", { "SwitchCase": 1 }],
|
||||||
"semi": [ 2, "always" ],
|
"semi": [2, "always"],
|
||||||
"keyword-spacing": [ 2, { "before": true, "after": true } ],
|
"keyword-spacing": [2, { "before": true, "after": true }],
|
||||||
"space-before-blocks": [ 2, "always" ],
|
"space-before-blocks": [2, "always"],
|
||||||
"no-mixed-spaces-and-tabs": [ 2, "smart-tabs" ],
|
"no-mixed-spaces-and-tabs": [2, "smart-tabs"],
|
||||||
"no-cond-assign": 0,
|
"no-cond-assign": 0,
|
||||||
"no-unused-vars": 2,
|
"no-unused-vars": 2,
|
||||||
"object-shorthand": [ 2, "always" ],
|
"object-shorthand": [2, "always"],
|
||||||
"no-const-assign": 2,
|
"no-const-assign": 2,
|
||||||
"no-class-assign": 2,
|
"no-class-assign": 2,
|
||||||
"no-this-before-super": 2,
|
"no-this-before-super": 2,
|
||||||
"no-var": 2,
|
"no-var": 2,
|
||||||
"no-unreachable": 2,
|
"no-unreachable": 2,
|
||||||
"valid-typeof": 2,
|
"valid-typeof": 2,
|
||||||
"quote-props": [ 2, "as-needed" ],
|
"quote-props": [2, "as-needed"],
|
||||||
"one-var": [ 2, "never" ],
|
"one-var": [2, "never"],
|
||||||
"prefer-arrow-callback": 2,
|
"prefer-arrow-callback": 2,
|
||||||
"prefer-const": [ 2, { "destructuring": "all" } ],
|
"prefer-const": [2, { "destructuring": "all" }],
|
||||||
"arrow-spacing": 2,
|
"arrow-spacing": 2,
|
||||||
"no-inner-declarations": 0
|
"no-inner-declarations": 0
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"es6": true,
|
"es6": true,
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"node": true,
|
"node": true,
|
||||||
"mocha": true
|
"mocha": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:import/errors",
|
"plugin:import/errors",
|
||||||
"plugin:import/warnings"
|
"plugin:import/warnings"
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": ["svelte3"],
|
||||||
"html"
|
"parserOptions": {
|
||||||
],
|
"ecmaVersion": 6,
|
||||||
"parserOptions": {
|
"sourceType": "module"
|
||||||
"ecmaVersion": 6,
|
},
|
||||||
"sourceType": "module"
|
"settings": {
|
||||||
},
|
"import/core-modules": ["svelte"],
|
||||||
"settings": {
|
"svelte3/extensions": ["html"]
|
||||||
"import/core-modules": [ "svelte" ]
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"rules": {
|
||||||
|
"indent": [2, "tab", { "SwitchCase": 1 }],
|
||||||
|
"semi": [2, "always"],
|
||||||
|
"keyword-spacing": [2, { "before": true, "after": true }],
|
||||||
|
"space-before-blocks": [2, "always"],
|
||||||
|
"no-mixed-spaces-and-tabs": [2, "smart-tabs"],
|
||||||
|
"no-cond-assign": 0,
|
||||||
|
"no-unused-vars": 2,
|
||||||
|
"object-shorthand": [2, "always"],
|
||||||
|
"no-const-assign": 2,
|
||||||
|
"no-class-assign": 2,
|
||||||
|
"no-this-before-super": 2,
|
||||||
|
"no-var": 2,
|
||||||
|
"no-unreachable": 2,
|
||||||
|
"valid-typeof": 2,
|
||||||
|
"quote-props": [2, "as-needed"],
|
||||||
|
"one-var": [2, "never"],
|
||||||
|
"prefer-arrow-callback": 2,
|
||||||
|
"prefer-const": [2, { "destructuring": "all" }],
|
||||||
|
"arrow-spacing": 2,
|
||||||
|
"no-inner-declarations": 0
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"es6": true,
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:import/errors",
|
||||||
|
"plugin:import/warnings"
|
||||||
|
],
|
||||||
|
"plugins": ["svelte3"],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 6,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"import/core-modules": ["svelte"],
|
||||||
|
"svelte3/extensions": [".html"]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
sudo: false
|
||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- "stable"
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- BUILD_TIMEOUT=10000
|
||||||
|
install:
|
||||||
|
- npm install
|
||||||
|
- npm install cypress
|
||||||
|
|
@ -0,0 +1,75 @@
|
|||||||
|
# sapper-template-rollup
|
||||||
|
|
||||||
|
A version of the default [Sapper](https://github.com/sveltejs/sapper) template that uses Rollup instead of webpack. To clone it and get started:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx degit sveltejs/sapper-template#rollup my-app
|
||||||
|
cd my-app
|
||||||
|
npm install # or yarn!
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open up [localhost:3000](http://localhost:3000) and start clicking around.
|
||||||
|
|
||||||
|
Consult [sapper.svelte.technology](https://sapper.svelte.technology) for help getting started.
|
||||||
|
|
||||||
|
*[Click here for the webpack version of this template](https://github.com/sveltejs/sapper-template)*
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
Sapper expects to find three directories in the root of your project — `app`, `assets` and `routes`.
|
||||||
|
|
||||||
|
|
||||||
|
### app
|
||||||
|
|
||||||
|
The [app](app) directory contains the entry points for your app — `client.js`, `server.js` and (optionally) a `service-worker.js` — along with a `template.html` file.
|
||||||
|
|
||||||
|
|
||||||
|
### assets
|
||||||
|
|
||||||
|
The [assets](assets) directory contains any static assets that should be available. These are served using [sirv](https://github.com/lukeed/sirv).
|
||||||
|
|
||||||
|
In your [service-worker.js](app/service-worker.js) file, you can import these as `assets` from the generated manifest...
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { assets } from './manifest/service-worker.js';
|
||||||
|
```
|
||||||
|
|
||||||
|
...so that you can cache them (though you can choose not to, for example if you don't want to cache very large files).
|
||||||
|
|
||||||
|
|
||||||
|
### routes
|
||||||
|
|
||||||
|
This is the heart of your Sapper app. There are two kinds of routes — *pages*, and *server routes*.
|
||||||
|
|
||||||
|
**Pages** are Svelte components written in `.html` files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.)
|
||||||
|
|
||||||
|
**Server routes** are modules written in `.js` files, that export functions corresponding to HTTP methods. Each function receives Express `request` and `response` objects as arguments, plus a `next` function. This is useful for creating a JSON API, for example.
|
||||||
|
|
||||||
|
There are three simple rules for naming the files that define your routes:
|
||||||
|
|
||||||
|
* A file called `routes/about.html` corresponds to the `/about` route. A file called `routes/blog/[slug].html` corresponds to the `/blog/:slug` route, in which case `params.slug` is available to the route
|
||||||
|
* The file `routes/index.html` (or `routes/index.js`) corresponds to the root of your app. `routes/about/index.html` is treated the same as `routes/about.html`.
|
||||||
|
* Files and directories with a leading underscore do *not* create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called `routes/_helpers/datetime.js` and it would *not* create a `/_helpers/datetime` route
|
||||||
|
|
||||||
|
|
||||||
|
## Rollup config
|
||||||
|
|
||||||
|
Sapper uses Rollup to provide code-splitting and dynamic imports, as well as compiling your Svelte components. As long as you don't do anything daft, you can edit the configuration files to add whatever plugins you'd like.
|
||||||
|
|
||||||
|
|
||||||
|
## Production mode and deployment
|
||||||
|
|
||||||
|
To start a production version of your app, run `npm run build && npm start`.
|
||||||
|
|
||||||
|
You can deploy your application to any environment that supports Node 8 or above. As an example, to deploy to [Now](https://zeit.co/now), run these commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g now
|
||||||
|
now
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Bugs and feedback
|
||||||
|
|
||||||
|
Sapper is in early development, and may have the odd rough edge here and there. Please be vocal over on the [Sapper issue tracker](https://github.com/sveltejs/sapper/issues).
|
@ -0,0 +1,18 @@
|
|||||||
|
version: "{build}"
|
||||||
|
|
||||||
|
shallow_clone: true
|
||||||
|
|
||||||
|
init:
|
||||||
|
- git config --global core.autocrlf false
|
||||||
|
|
||||||
|
build: off
|
||||||
|
|
||||||
|
environment:
|
||||||
|
matrix:
|
||||||
|
# node.js
|
||||||
|
- nodejs_version: stable
|
||||||
|
|
||||||
|
install:
|
||||||
|
- ps: Install-Product node $env:nodejs_version
|
||||||
|
- npm install cypress
|
||||||
|
- npm install
|
@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
title: The easiest way to get started with Svelte
|
||||||
|
description: This'll only take a minute.
|
||||||
|
pubdate: 2017-08-07
|
||||||
|
author: Rich Harris
|
||||||
|
authorURL: https://twitter.com/Rich_Harris
|
||||||
|
---
|
||||||
|
|
||||||
|
Svelte is a [new kind of framework](/blog/frameworks-without-the-framework). Rather than putting a `<script src='svelte.js'>` tag on the page, or bringing it into your app with `import` or `require`, Svelte is a compiler that works behind the scenes to turn your component files into beautifully optimised JavaScript.
|
||||||
|
|
||||||
|
Because of that, getting started with it can be a little bit confusing at first. How, you might reasonably ask, do you make a Svelte app?
|
||||||
|
|
||||||
|
|
||||||
|
## 1. Use the REPL
|
||||||
|
|
||||||
|
The [Svelte REPL](https://svelte.technology/repl) is the easiest way to begin. You can choose from a list of examples to get you started, and tweak them until they do what you want.
|
||||||
|
|
||||||
|
<aside>You'll need to have [Node.js](https://nodejs.org/) installed, and know how to use the terminal</aside>
|
||||||
|
|
||||||
|
At some point, your app will outgrow the REPL. Click the **download** button to save a `svelte-app.zip` file to your computer and uncompress it.
|
||||||
|
|
||||||
|
Open a terminal window and set the project up...
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/svelte-app
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
...then start up a development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This will serve your app on [localhost:5000](http://localhost:5000) and rebuild it with [Rollup](https://rollupjs.org) every time you make a change to the files in `svelte-app/src`.
|
||||||
|
|
||||||
|
|
||||||
|
## 2. Use degit
|
||||||
|
|
||||||
|
When you download from the REPL, you're getting a customised version of the [sveltejs/template](https://github.com/sveltejs/template) repo. You can skip messing around with zip files by using [degit](https://github.com/Rich-Harris/degit), a project scaffolding tool.
|
||||||
|
|
||||||
|
In the terminal, install degit globally (you only need to do this once):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g degit
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, you can instantly create a new project like so:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
degit sveltejs/template my-new-project
|
||||||
|
cd my-new-project
|
||||||
|
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Once you've tinkered a bit and understood how everything fits together, you can fork [sveltejs/template](https://github.com/sveltejs/template) and start doing this instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
degit your-name/template my-new-project
|
||||||
|
```
|
||||||
|
|
||||||
|
And that's it! Do `npm run build` to create a production-ready version of your app, and check the project template's [README](https://github.com/sveltejs/template/blob/master/README.md) for instructions on how to easily deploy your app to the web with [Now](https://zeit.co/now) or [Surge](http://surge.sh/).
|
||||||
|
|
||||||
|
You're not restricted to using Rollup — there are also integrations for [webpack](https://github.com/sveltejs/svelte-loader), [Browserify](https://github.com/tehshrike/sveltify) and others, or you can use the [Svelte CLI](https://github.com/sveltejs/svelte-cli) or the [API](https://github.com/sveltejs/svelte#api) directly. If you make a project template using one of these tools, please share it with the [Svelte Gitter chatroom](https://gitter.im/sveltejs/svelte), or via [@sveltejs](https://twitter.com/sveltejs) on Twitter!
|
@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
title: Sapper: Towards the ideal web app framework
|
||||||
|
description: Taking the next-plus-one step
|
||||||
|
pubdate: 2017-12-31
|
||||||
|
author: Rich Harris
|
||||||
|
authorURL: https://twitter.com/Rich_Harris
|
||||||
|
---
|
||||||
|
|
||||||
|
> Quickstart for the impatient: [the Sapper docs](https://sapper.svelte.technology), and the [starter template](https://github.com/sveltejs/sapper-template)
|
||||||
|
|
||||||
|
If you had to list the characteristics of the perfect Node.js web application framework, you'd probably come up with something like this:
|
||||||
|
|
||||||
|
1. It should do server-side rendering, for fast initial loads and no caveats around SEO
|
||||||
|
2. As a corollary, your app's codebase should be universal — write once for server *and* client
|
||||||
|
3. The client-side app should *hydrate* the server-rendered HTML, attaching event listeners (and so on) to existing elements rather than re-rendering them
|
||||||
|
4. Navigating to subsequent pages should be instantaneous
|
||||||
|
5. Offline, and other Progressive Web App characteristics, must be supported out of the box
|
||||||
|
6. Only the JavaScript and CSS required for the first page should load initially. That means the framework should do automatic code-splitting at the route level, and support dynamic `import(...)` for more granular manual control
|
||||||
|
7. No compromise on performance
|
||||||
|
8. First-rate developer experience, with hot module reloading and all the trimmings
|
||||||
|
9. The resulting codebase should be easy to grok and maintain
|
||||||
|
10. It should be possible to understand and customise every aspect of the system — no webpack configs locked up in the framework, and as little hidden 'plumbing' as possible
|
||||||
|
11. Learning the entire framework in under an hour should be easy, and not just for experienced developers
|
||||||
|
|
||||||
|
[Next.js](https://github.com/zeit/next.js) is close to this ideal. If you haven't encountered it yet, I strongly recommend going through the tutorials at [learnnextjs.com](https://learnnextjs.com). Next introduced a brilliant idea: all the pages of your app are files in a `your-project/pages` directory, and each of those files is just a React component.
|
||||||
|
|
||||||
|
Everything else flows from that breakthrough design decision. Finding the code responsible for a given page is easy, because you can just look at the filesystem rather than playing 'guess the component name'. Project structure bikeshedding is a thing of the past. And the combination of SSR (server-side rendering) and code-splitting — something the React Router team [gave up on](https://reacttraining.com/react-router/web/guides/code-splitting), declaring 'Godspeed those who attempt the server-rendered, code-split apps' — is trivial.
|
||||||
|
|
||||||
|
But it's not perfect. As churlish as it might be to list the flaws in something *so, so good*, there are some:
|
||||||
|
|
||||||
|
* Next uses something called 'route masking' to create nice URLs (e.g. `/blog/hello-world` instead of `/post?slug=hello-world`). This undermines the guarantee about directory structure corresponding to app structure, and forces you to maintain configuration that translates between the two forms
|
||||||
|
* All your routes are assumed to be universal 'pages'. But it's very common to need routes that only render on the server, such as a 301 redirect or an [API endpoint](/api/blog/sapper-towards-the-ideal-web-app-framework) that serves the data for your pages, and Next doesn't have a great solution for this. You can add logic to your `server.js` file to handle these cases, but it feels at odds with the declarative approach taken for pages
|
||||||
|
* To use the client-side router, links can't be standard `<a>` tags. Instead, you have to use framework-specific `<Link>` components, which is impossible in the markdown content for a blog post such as this one, for example
|
||||||
|
|
||||||
|
The real problem, though, is that all that goodness comes for a price. The simplest possible Next app — a single 'hello world' page that renders some static text — involves 66kb of gzipped JavaScript. Unzipped, it's 204kb, which is a non-trivial amount of code for a mobile device to parse at a time when performance is a critical factor determining whether or not your users will stick around. And that's the *baseline*.
|
||||||
|
|
||||||
|
We can do better!
|
||||||
|
|
||||||
|
|
||||||
|
## The compiler-as-framework paradigm shift
|
||||||
|
|
||||||
|
[Svelte introduced a radical idea](https://svelte.technology/blog/frameworks-without-the-framework): what if your UI framework wasn't a framework at all, but a compiler that turned your components into standalone JavaScript modules? Instead of using a library like React or Vue, which knows nothing about your app and must therefore be a one-size-fits-all solution, we can ship highly-optimised vanilla JavaScript. Just the code your app needs, and without the memory and performance overhead of solutions based on a virtual DOM.
|
||||||
|
|
||||||
|
The JavaScript world is [moving towards this model](https://tomdale.net/2017/09/compilers-are-the-new-frameworks/). [Stencil](https://stenciljs.com), a Svelte-inspired framework from the Ionic team, compiles to web components. [Glimmer](https://glimmerjs.com) *doesn't* compile to standalone JavaScript (the pros and cons of which deserve a separate blog post), but the team is doing some fascinating research around compiling templates to bytecode. (React is [getting in on the action](https://twitter.com/trueadm/status/944908776896978946), though their current research focuses on optimising your JSX app code, which is arguably more similar to the ahead-of-time optimisations that Angular, Ractive and Vue have been doing for a few years.)
|
||||||
|
|
||||||
|
What happens if we use the new model as a starting point?
|
||||||
|
|
||||||
|
|
||||||
|
## Introducing Sapper
|
||||||
|
|
||||||
|
<aside>The [name comes from](https://sapper.svelte.technology/guide#why-the-name-) the term for combat engineers, and is also short for <strong>S</strong>velte <strong>app</strong> mak<strong>er</strong></aside>
|
||||||
|
|
||||||
|
[Sapper](https://sapper.svelte.technology) is the answer to that question. **Sapper is a Next.js-style framework that aims to meet the eleven criteria at the top of this article while dramatically reducing the amount of code that gets sent to the browser.** It's implemented as Express-compatible middleware, meaning it's easy to understand and customise.
|
||||||
|
|
||||||
|
The same 'hello world' app that took 204kb with React and Next weighs just 7kb with Sapper. That number is likely to fall further in the future as we explore the space of optimisation possibilities, such as not shipping any JavaScript *at all* for pages that aren't interactive, beyond the tiny Sapper runtime that handles client-side routing.
|
||||||
|
|
||||||
|
What about a more 'real world' example? Conveniently, the [RealWorld](https://github.com/gothinkster/realworld) project, which challenges frameworks to develop an implementation of a Medium clone, gives us a way to find out. The [Sapper implementation](http://svelte-realworld.now.sh/) takes 39.6kb (11.8kb zipped) to render an interactive homepage.
|
||||||
|
|
||||||
|
<aside>Code-splitting isn't free — if the reference implementation used code-splitting, it would be larger still</aside>
|
||||||
|
|
||||||
|
The entire app costs 132.7kb (39.9kb zipped), which is significantly smaller than the reference React/Redux implementation at 327kb (85.7kb), but even if was as large it would *feel* faster because of code-splitting. And that's a crucial point. We're told we need to code-split our apps, but if your app uses a traditional framework like React or Vue then there's a hard lower bound on the size of your initial code-split chunk — the framework itself, which is likely to be a significant portion of your total app size. With the Svelte approach, that's no longer the case.
|
||||||
|
|
||||||
|
But size is only part of the story. Svelte apps are also extremely performant and memory-efficient, and the framework includes powerful features that you would sacrifice if you chose a 'minimal' or 'simple' UI library.
|
||||||
|
|
||||||
|
|
||||||
|
## Trade-offs
|
||||||
|
|
||||||
|
The biggest drawback for many developers evaluating Sapper would be 'but I like React, and I already know how to use it', which is fair.
|
||||||
|
|
||||||
|
If you're in that camp, I'd invite you to at least try alternative frameworks. You might be pleasantly surprised! The [Sapper RealWorld](https://github.com/sveltejs/realworld) implementation totals 1,201 lines of source code, compared to 2,377 for the reference implementation, because you're able to express concepts very concisely using Svelte's template syntax (which [takes all of five minutes to master](https://svelte.technology/guide#template-syntax)). You get [scoped CSS](the-zen-of-just-writing-css), with unused style removal and minification built-in, and you can use preprocessors like LESS if you want. You no longer need to use Babel. SSR is ridiculously fast, because it's just string concatenation. And we recently introduced [svelte/store](https://svelte.technology/guide#state-management), a tiny global store that synchronises state across your component hierarchy with zero boilerplate. The worst that can happen is that you'll end up feeling vindicated!
|
||||||
|
|
||||||
|
But there are trade-offs nonetheless. Some people have a pathological aversion to any form of 'template language', and maybe that applies to you. JSX proponents will clobber you with the 'it's just JavaScript' mantra, and therein lies React's greatest strength, which is that it is infinitely flexible. That flexibility comes with its own set of trade-offs, but we're not here to discuss those.
|
||||||
|
|
||||||
|
And then there's *ecosystem*. The universe around React in particular — the devtools, editor integrations, ancillary libraries, tutorials, StackOverflow answers, hell, even job opportunities — is unrivalled. While it's true that citing 'ecosystem' as the main reason to choose a tool is a sign that you're stuck on a local maximum, apt to be marooned by the rising waters of progress, it's still a major point in favour of incumbents.
|
||||||
|
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
We're not at version 1.0.0 yet, and a few things may change before we get there. Once we do (soon!), there are a lot of exciting possibilities.
|
||||||
|
|
||||||
|
I believe the next frontier of web performance is 'whole-app optimisation'. Currently, Svelte's compiler operates at the component level, but a compiler that understood the boundaries *between* those components could generate even more efficient code. The React team's [Prepack research](https://twitter.com/trueadm/status/944908776896978946) is predicated on a similar idea, and the Glimmer team is doing some interesting work in this space. Svelte and Sapper are well positioned to take advantage of these ideas.
|
||||||
|
|
||||||
|
Speaking of Glimmer, the idea of compiling components to bytecode is one that we'll probably steal in 2018. A framework like Sapper could conceivably determine which compilation mode to use based on the characteristics of your app. It could even serve JavaScript for the initial route for the fastest possible startup time, then lazily serve a bytecode interpreter for subsequent routes, resulting in the optimal combination of startup size and total app size.
|
||||||
|
|
||||||
|
Mostly, though, we want the direction of Sapper to be determined by its users. If you're the kind of developer who enjoys life on the bleeding edge and would like to help shape the future of how we build web apps, please join us on [GitHub](https://github.com/sveltejs/svelte) and [Gitter](https://gitter.im/sveltejs/svelte).
|
@ -0,0 +1,218 @@
|
|||||||
|
---
|
||||||
|
title: Svelte v2 is out!
|
||||||
|
description: Here's what you need to know
|
||||||
|
pubdate: 2018-04-18
|
||||||
|
author: Rich Harris
|
||||||
|
authorURL: https://twitter.com/Rich_Harris
|
||||||
|
---
|
||||||
|
|
||||||
|
<aside>Our motto is 'move slowly and break things'. No, wait, that came out wrong...</aside>
|
||||||
|
|
||||||
|
Almost a year after we first started talking about version 2 on the Svelte issue tracker, it's finally time to make some breaking changes. This blog post will explain what changed, why it changed, and what you need to do to bring your apps up to date.
|
||||||
|
|
||||||
|
|
||||||
|
## tl;dr
|
||||||
|
|
||||||
|
Each of these items is described in more depth below. If you get stuck, ask for help in our friendly [Gitter chatroom](https://gitter.im/sveltejs/svelte).
|
||||||
|
|
||||||
|
<style>
|
||||||
|
ul {
|
||||||
|
position: relative;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li input {
|
||||||
|
position: absolute;
|
||||||
|
left: -2.5em;
|
||||||
|
top: 0.3em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
- <input type=checkbox> Install Svelte v2 from npm
|
||||||
|
- <input type=checkbox> Upgrade your templates with [svelte-upgrade](https://github.com/sveltejs/svelte-upgrade)
|
||||||
|
- <input type=checkbox> Remove calls to `component.observe`, or add the `observe` method from [svelte-extras](https://github.com/sveltejs/svelte-extras)
|
||||||
|
- <input type=checkbox> Rewrite calls to `component.get('foo')` as `component.get().foo`
|
||||||
|
- <input type=checkbox> Return `destroy` from your custom event handlers, rather than `teardown`
|
||||||
|
- <input type=checkbox> Make sure you're not passing numeric string props to components
|
||||||
|
|
||||||
|
|
||||||
|
## New template syntax
|
||||||
|
|
||||||
|
The most visible change: we've made some improvements to the template syntax.
|
||||||
|
|
||||||
|
A common piece of feedback we heard was 'ewww, Mustache' or 'ewww, Handlebars'. A lot of people who used string-based templating systems in a previous era of web development *really* dislike them. Because Svelte adopted the `{{curlies}}` from those languages, a lot of people assumed that we somehow shared the limitations of those tools, such as weird scoping rules or an inability to use arbitrary JavaScript expressions.
|
||||||
|
|
||||||
|
<aside>If you need to show an actual `{` character, it's as easy as `&#123;`</aside>
|
||||||
|
|
||||||
|
Beyond that, JSX proved that double curlies are unnecessary. So we've made our templates more... svelte, by adopting single curlies. The result feels much lighter to look at and is more pleasant to type:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h1>Hello {name}!</h1>
|
||||||
|
```
|
||||||
|
|
||||||
|
There are a few other updates. But you don't need to make them manually — just run [svelte-upgrade](https://github.com/sveltejs/svelte-upgrade) on your codebase:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx svelte-upgrade v2 src
|
||||||
|
```
|
||||||
|
|
||||||
|
This assumes any `.html` files in `src` are Svelte components. You can specify whichever directory you like, or target a different directory — for example, you'd do `npx svelte-upgrade v2 routes` to update a [Sapper](https://sapper.svelte.technology) app.
|
||||||
|
|
||||||
|
To see the full set of changes, consult the [svelte-upgrade README](https://github.com/sveltejs/svelte-upgrade#svelte-v2-syntax-changes).
|
||||||
|
|
||||||
|
|
||||||
|
## Computed properties
|
||||||
|
|
||||||
|
Another thing that people often found confusing about Svelte is the way computed properties work. To recap, if you had a component with this...
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
d: (a, b, c) => a = b + c
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
...then Svelte would first look at the function arguments to see which values `d` depended on, and then it would write code that updated `d` whenever those values changed, by injecting them into the function. That's cool, because it allows you to derive complex values from your component's inputs without worrying about when they need to recomputed, but it's also... *weird*. JavaScript doesn't work that way!
|
||||||
|
|
||||||
|
In v2, we use [destructuring](http://www.jstips.co/en/javascript/use-destructuring-in-function-parameters/) instead:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
d: ({ a, b, c }) => a = b + c
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The Svelte compiler can still see which values `d` depends on, but it's no longer injecting values — it just passes the component state object into each computed property.
|
||||||
|
|
||||||
|
Again, you don't need to make this change manually — just run svelte-upgrade on your components, as shown above.
|
||||||
|
|
||||||
|
|
||||||
|
## Sorry, IE11. It's not you, it's... well actually, yeah. It's you
|
||||||
|
|
||||||
|
Svelte v1 was careful to only emit ES5 code, so that you wouldn't be forced to faff around with transpilers in order to use it. But it's 2018 now, and almost all browsers support modern JavaScript. By ditching the ES5 constraint, we can generate leaner code.
|
||||||
|
|
||||||
|
If you need to support IE11 and friends, you will need to use a transpiler like [Babel](http://babeljs.io/repl) or [Bublé](http://buble.surge.sh/).
|
||||||
|
|
||||||
|
|
||||||
|
## New lifecycle hooks
|
||||||
|
|
||||||
|
In addition to `oncreate` and `ondestroy`, Svelte v2 adds two more [lifecycle hooks](guide#lifecycle-hooks) for responding to state changes:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default {
|
||||||
|
onstate({ changed, current, previous }) {
|
||||||
|
// this fires before oncreate, and
|
||||||
|
// whenever state changes
|
||||||
|
},
|
||||||
|
|
||||||
|
onupdate({ changed, current, previous }) {
|
||||||
|
// this fires after oncreate, and
|
||||||
|
// whenever the DOM has been updated
|
||||||
|
// following a state change
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also listen to those events programmatically:
|
||||||
|
|
||||||
|
```js
|
||||||
|
component.on('state', ({ changed, current, previous }) => {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## component.observe
|
||||||
|
|
||||||
|
With the new lifecycle hooks, we no longer need the `component.observe(...)` method:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// before
|
||||||
|
export default {
|
||||||
|
oncreate() {
|
||||||
|
this.observe('foo', foo => {
|
||||||
|
console.log(`foo is now ${foo}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// after
|
||||||
|
export default {
|
||||||
|
onstate({ changed, current }) {
|
||||||
|
if (changed.foo) {
|
||||||
|
console.log(`foo is now ${current.foo}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This shrinks the amount of code Svelte needs to generate, and gives you more flexibility. For example, it's now very easy to take action when any one of *several* properties have changed, such as redrawing a canvas without debouncing several observers.
|
||||||
|
|
||||||
|
However, if you prefer to use `component.observe(...)`, then you can install it from [svelte-extras](https://github.com/sveltejs/svelte-extras):
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { observe } from 'svelte-extras';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
observe
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## component.get
|
||||||
|
|
||||||
|
This method no longer takes an optional `key` argument — instead, it always returns the entire state object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// before
|
||||||
|
const foo = this.get('foo');
|
||||||
|
const bar = this.get('bar');
|
||||||
|
|
||||||
|
// after
|
||||||
|
const { foo, bar } = this.get();
|
||||||
|
```
|
||||||
|
|
||||||
|
This change might seem annoying initially, but it's the right move: among other things, it's likely to play better with type systems as we explore that space more fully in future.
|
||||||
|
|
||||||
|
|
||||||
|
## event_handler.destroy
|
||||||
|
|
||||||
|
If your app has [custom event handlers](guide#custom-event-handlers), they must return an object with a `destroy` method, *not* a `teardown` method (this aligns event handlers with the component API).
|
||||||
|
|
||||||
|
|
||||||
|
## No more type coercion
|
||||||
|
|
||||||
|
Previously, numeric values passed to components were treated as numbers:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<Counter start='1'/>
|
||||||
|
```
|
||||||
|
|
||||||
|
That causes unexpected behaviour, and has been changed: if you need to pass a literal number, do so as an expression:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<Counter start={1}/>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Compiler changes
|
||||||
|
|
||||||
|
In most cases you'll never need to deal with the compiler directly, so this shouldn't require any action on your part. It's worth noting anyway: the compiler API has changed. Instead of an object with a mish-mash of properties, the compiler now returns `js`, `css`, `ast` and `stats`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { js, css, ast, stats } = svelte.compile(source, options);
|
||||||
|
```
|
||||||
|
|
||||||
|
`js` and `css` are both `{ code, map }` objects, where `code` is a string and `map` is a sourcemap. The `ast` is an abstract syntax tree of your component, and the `stats` object contains metadata about the component, and information about the compilation.
|
||||||
|
|
||||||
|
Before, there was a `svelte.validate` method which checked your component was valid. That's been removed — if you want to check a component without actually compiling it, just pass the `generate: false` option.
|
||||||
|
|
||||||
|
|
||||||
|
## My app is broken! Help!
|
||||||
|
|
||||||
|
Hopefully this covers everything, and the update should be easier for you than it was for us. But if you find bugs, or discover things that aren't mentioned here, swing by [Gitter](https://gitter.im/sveltejs/svelte) or raise an issue on the [tracker](https://github.com/sveltejs/svelte/issues).
|
@ -0,0 +1,125 @@
|
|||||||
|
<!--
|
||||||
|
https://eugenkiss.github.io/7guis/tasks#circle
|
||||||
|
|
||||||
|
Click on the canvas to draw a circle. Click on a circle
|
||||||
|
to select it. Right-click on the canvas to adjust the
|
||||||
|
radius of the selected circle.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let i = 0;
|
||||||
|
let undoStack = [[]];
|
||||||
|
let circles = [];
|
||||||
|
let selected;
|
||||||
|
let adjusting = false;
|
||||||
|
let adjusted = false;
|
||||||
|
|
||||||
|
function handleClick(event) {
|
||||||
|
if (adjusting) {
|
||||||
|
adjusting = false;
|
||||||
|
|
||||||
|
// if circle was adjusted,
|
||||||
|
// push to the stack
|
||||||
|
if (adjusted) push();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const circle = {
|
||||||
|
cx: event.clientX,
|
||||||
|
cy: event.clientY,
|
||||||
|
r: 50
|
||||||
|
};
|
||||||
|
|
||||||
|
circles = circles.concat(circle);
|
||||||
|
selected = circle;
|
||||||
|
|
||||||
|
push();
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjust(event) {
|
||||||
|
selected.r = +event.target.value;
|
||||||
|
circles = circles;
|
||||||
|
adjusted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function select(circle, event) {
|
||||||
|
if (!adjusting) {
|
||||||
|
event.stopPropagation();
|
||||||
|
selected = circle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function push() {
|
||||||
|
const newUndoStack = undoStack.slice(0, ++i);
|
||||||
|
newUndoStack.push(clone(circles));
|
||||||
|
undoStack = newUndoStack;
|
||||||
|
}
|
||||||
|
|
||||||
|
function travel(d) {
|
||||||
|
circles = clone(undoStack[i += d]);
|
||||||
|
adjusting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clone(circles) {
|
||||||
|
return circles.map(({ cx, cy, r }) => ({ cx, cy, r }));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
background-color: #eee;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
circle {
|
||||||
|
stroke: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adjuster {
|
||||||
|
position: absolute;
|
||||||
|
width: 80%;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
padding: 1em;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(255,255,255,0.7);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='range'] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button on:click="{() => travel(-1)}" disabled="{i === 0}">undo</button>
|
||||||
|
<button on:click="{() => travel(+1)}" disabled="{i === undoStack.length -1}">redo</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svg on:click={handleClick} >
|
||||||
|
{#each circles as circle}
|
||||||
|
<circle cx={circle.cx} cy={circle.cy} r={circle.r}
|
||||||
|
on:click="{event => select(circle, event)}"
|
||||||
|
on:contextmenu|stopPropagation|preventDefault="{() => {
|
||||||
|
adjusting = !adjusting;
|
||||||
|
if (adjusting) selected = circle;
|
||||||
|
}}"
|
||||||
|
fill="{circle === selected ? '#ccc': 'white'}"
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{#if adjusting}
|
||||||
|
<div class="adjuster">
|
||||||
|
<p>adjust diameter of circle at {selected.cx}, {selected.cy}</p>
|
||||||
|
<input type="range" value={selected.r} on:input={adjust}>
|
||||||
|
</div>
|
||||||
|
{/if}
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,3 @@
|
|||||||
|
<!-- https://github.com/eugenkiss/7guis/wiki#counter -->
|
||||||
|
<input type=number bind:value={count}>
|
||||||
|
<button on:click="{() => count += 1}">count</button>
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"count": 0
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
<!-- https://eugenkiss.github.io/7guis/tasks#crud -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { beforeUpdate } from 'svelte';
|
||||||
|
|
||||||
|
export let people = [];
|
||||||
|
|
||||||
|
let filteredPeople;
|
||||||
|
let selected;
|
||||||
|
let prefix = '';
|
||||||
|
let first = '';
|
||||||
|
let last = '';
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
function create() {
|
||||||
|
people = people.concat({ first, last });
|
||||||
|
i = people.length - 1;
|
||||||
|
first = last = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
people[i] = { first, last };
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove() {
|
||||||
|
people = [...people.slice(0, i), ...people.slice(i + 1)];
|
||||||
|
|
||||||
|
first = last = '';
|
||||||
|
i = Math.min(i, people.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: filteredPeople = prefix
|
||||||
|
? people.filter(person => {
|
||||||
|
const name = `${person.last}, ${person.first}`;
|
||||||
|
return name.toLowerCase().startsWith(prefix.toLowerCase());
|
||||||
|
})
|
||||||
|
: people;
|
||||||
|
|
||||||
|
$: selected = filteredPeople[i];
|
||||||
|
|
||||||
|
$: if (selected) {
|
||||||
|
first = selected.first;
|
||||||
|
last = selected.last;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
float: left;
|
||||||
|
margin: 0 1em 1em 0;
|
||||||
|
width: 14em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<input placeholder="filter prefix" bind:value={prefix}>
|
||||||
|
|
||||||
|
<select bind:value={i} size={5}>
|
||||||
|
{#each filteredPeople as person, i}
|
||||||
|
<option value={i}>{person.last}, {person.first}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label><input bind:value={first} placeholder="first"></label>
|
||||||
|
<label><input bind:value={last} placeholder="last"></label>
|
||||||
|
|
||||||
|
<div class='buttons'>
|
||||||
|
<button on:click={create} disabled="{!first || !last}">create</button>
|
||||||
|
<button on:click={update} disabled="{!first || !last || !selected}">update</button>
|
||||||
|
<button on:click={remove} disabled="{!selected}">delete</button>
|
||||||
|
</div>
|
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"people": [
|
||||||
|
{
|
||||||
|
"first": "Hans",
|
||||||
|
"last": "Emil"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"first": "Max",
|
||||||
|
"last": "Mustermann"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"first": "Roman",
|
||||||
|
"last": "Tisch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
<script>
|
||||||
|
const tomorrow = new Date(Date.now() + 86400000);
|
||||||
|
|
||||||
|
const tomorrowAsString = [
|
||||||
|
tomorrow.getFullYear(),
|
||||||
|
pad(tomorrow.getMonth() + 1, 2),
|
||||||
|
pad(tomorrow.getDate(), 2)
|
||||||
|
].join('-');
|
||||||
|
|
||||||
|
let start = tomorrowAsString;
|
||||||
|
let end = tomorrowAsString;
|
||||||
|
let isReturn = false;
|
||||||
|
|
||||||
|
const startDate = () => convertToDate(start);
|
||||||
|
const endDate = () => convertToDate(end);
|
||||||
|
|
||||||
|
|
||||||
|
function bookFlight() {
|
||||||
|
const type = isReturn ? 'return' : 'one-way';
|
||||||
|
|
||||||
|
let message = `You have booked a ${type} flight, leaving ${startDate().toDateString()}`;
|
||||||
|
if (type === 'return') {
|
||||||
|
message += ` and returning ${endDate().toDateString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToDate(str) {
|
||||||
|
var split = str.split('-');
|
||||||
|
return new Date(+split[0], +split[1] - 1, +split[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(x, len) {
|
||||||
|
x = String(x);
|
||||||
|
while (x.length < len) x = `0${x}`;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
select, input, button {
|
||||||
|
display: block;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- https://github.com/eugenkiss/7guis/wiki#flight-booker -->
|
||||||
|
<select bind:value={isReturn}>
|
||||||
|
<option value={false}>one-way flight</option>
|
||||||
|
<option value={true}>return flight</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<input type=date bind:value={start}>
|
||||||
|
<input type=date bind:value={end} disabled={!isReturn}>
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={bookFlight}
|
||||||
|
disabled="{isReturn && (startDate() >= endDate())}"
|
||||||
|
>book</button>
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,24 @@
|
|||||||
|
<!-- https://github.com/eugenkiss/7guis/wiki#temperature-converter -->
|
||||||
|
<input value={c} on:input="{e => setBothFromC(e.target.value)}" type=number> °c =
|
||||||
|
<input value={f} on:input="{e => setBothFromF(e.target.value)}" type=number> °f
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let c = 0;
|
||||||
|
let f = 32;
|
||||||
|
|
||||||
|
function setBothFromC(value) {
|
||||||
|
c = +value;
|
||||||
|
f = +(32 + (9 / 5 * c)).toFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBothFromF(value) {
|
||||||
|
f = +value;
|
||||||
|
c =+(5 / 9 * (f - 32)).toFixed(1);
|
||||||
|
}
|
||||||
|
</script>
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"celsius": 0
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
<!-- https://eugenkiss.github.io/7guis/tasks#timer -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
let elapsed = 0;
|
||||||
|
let duration = 5000;
|
||||||
|
|
||||||
|
let last_time = window.performance.now();
|
||||||
|
let frame;
|
||||||
|
|
||||||
|
(function update() {
|
||||||
|
frame = requestAnimationFrame(update);
|
||||||
|
|
||||||
|
const time = window.performance.now();
|
||||||
|
elapsed += Math.min(
|
||||||
|
time - last_time,
|
||||||
|
duration - elapsed
|
||||||
|
);
|
||||||
|
|
||||||
|
last_time = time;
|
||||||
|
}());
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
cancelAnimationFrame(frame);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
elapsed time:
|
||||||
|
<progress value="{elapsed / duration}"></progress>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div>{(elapsed / 1000).toFixed(1)}s</div>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
duration:
|
||||||
|
<input type="range" bind:value={duration} min="1" max="20000">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button on:click="{() => elapsed = 0}">reset</button>
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,24 @@
|
|||||||
|
<script>
|
||||||
|
export let promise;
|
||||||
|
|
||||||
|
// [svelte-upgrade suggestion]
|
||||||
|
// review these functions and remove unnecessary 'export' keywords
|
||||||
|
export function findAnswer() {
|
||||||
|
promise = new Promise(fulfil => {
|
||||||
|
const delay = 1000 + Math.random() * 3000;
|
||||||
|
setTimeout(() => fulfil(42), delay);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click='{findAnswer}'>find the answer</button>
|
||||||
|
|
||||||
|
{#if promise}
|
||||||
|
{#await promise}
|
||||||
|
<p>wait for it...</p>
|
||||||
|
{:then answer}
|
||||||
|
<p>the answer is {answer}!</p>
|
||||||
|
{:catch error}
|
||||||
|
<p>well that's odd</p>
|
||||||
|
{/await}
|
||||||
|
{/if}
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,112 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { scaleLinear } from 'd3-scale';
|
||||||
|
|
||||||
|
export let points;
|
||||||
|
|
||||||
|
const xTicks = [1990, 1995, 2000, 2005, 2010, 2015];
|
||||||
|
const yTicks = [0, 5, 10, 15, 20];
|
||||||
|
const padding = { top: 20, right: 15, bottom: 20, left: 25 };
|
||||||
|
|
||||||
|
let width = 500;
|
||||||
|
let height = 200;
|
||||||
|
let barWidth;
|
||||||
|
let xScale;
|
||||||
|
let yScale;
|
||||||
|
|
||||||
|
function formatMobile(tick) {
|
||||||
|
return "'" + tick % 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: xScale = scaleLinear()
|
||||||
|
.domain([0, xTicks.length])
|
||||||
|
.range([padding.left, width - padding.right]);
|
||||||
|
|
||||||
|
$: yScale = scaleLinear()
|
||||||
|
.domain([0, Math.max.apply(null, yTicks)])
|
||||||
|
.range([height - padding.bottom, padding.top]);
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const innerWidth = width - (padding.left + padding.right);
|
||||||
|
barWidth = innerWidth / xTicks.length;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick {
|
||||||
|
font-family: Helvetica, Arial;
|
||||||
|
font-size: .725em;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick line {
|
||||||
|
stroke: #e2e2e2;
|
||||||
|
stroke-dasharray: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick text {
|
||||||
|
fill: #ccc;
|
||||||
|
text-anchor: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick.tick-0 line {
|
||||||
|
stroke-dasharray: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-axis .tick text {
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bars rect {
|
||||||
|
fill: #a11;
|
||||||
|
stroke: none;
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="chart">
|
||||||
|
<h2>US birthrate by year {width}/{height}</h2>
|
||||||
|
<svg bind:clientWidth={width} bind:clientHeight={height}>
|
||||||
|
<!-- y axis -->
|
||||||
|
<g class="axis y-axis" transform="translate(0,{padding.top})">
|
||||||
|
{#each yTicks as tick}
|
||||||
|
<g class="tick tick-{tick}" transform="translate(0, {yScale(tick) - padding.bottom})">
|
||||||
|
<line x2="100%"></line>
|
||||||
|
<text y="-4">{tick} {tick === 20 ? ' per 1,000 population' : ''}</text>
|
||||||
|
</g>
|
||||||
|
{/each}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- x axis -->
|
||||||
|
<g class="axis x-axis">
|
||||||
|
{#each points as point, i}
|
||||||
|
<g class="tick" transform="translate({xScale(i)},{height})">
|
||||||
|
<text x="{barWidth/2}" y="-4">{width > 380 ? point.year : formatMobile(point.year)}</text>
|
||||||
|
</g>
|
||||||
|
{/each}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g class='bars'>
|
||||||
|
{#each points as point, i}
|
||||||
|
<rect
|
||||||
|
x="{xScale(i) + 2}"
|
||||||
|
y="{yScale(point.birthrate)}"
|
||||||
|
width="{barWidth - 4}"
|
||||||
|
height="{height - padding.bottom - yScale(point.birthrate)}"
|
||||||
|
></rect>
|
||||||
|
{/each}
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"points": [
|
||||||
|
{
|
||||||
|
"year": 1990,
|
||||||
|
"birthrate": 16.7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"year": 1995,
|
||||||
|
"birthrate": 14.6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"year": 2000,
|
||||||
|
"birthrate": 14.4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"year": 2005,
|
||||||
|
"birthrate": 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"year": 2010,
|
||||||
|
"birthrate": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"year": 2015,
|
||||||
|
"birthrate": 12.4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
<label>
|
||||||
|
<input type=checkbox bind:group={selected} value="red">
|
||||||
|
red
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type=checkbox bind:group={selected} value="green">
|
||||||
|
green
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type=checkbox bind:group={selected} value="blue">
|
||||||
|
blue
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p>{selected.join(', ') || 'nothing'} selected</p>
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"selected": ["blue"]
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
{#each todos as todo}
|
||||||
|
<div class="todo {todo.done ? 'done': ''}">
|
||||||
|
<input type=checkbox bind:checked={todo.done}>
|
||||||
|
<input type=text bind:value={todo.description}>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input[type="text"] {
|
||||||
|
width: 20em;
|
||||||
|
max-width: calc(100% - 2em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.done {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"todos": [
|
||||||
|
{
|
||||||
|
"description": "Buy some milk",
|
||||||
|
"done": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Do the laundry",
|
||||||
|
"done": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Find life's true purpose",
|
||||||
|
"done": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
<!-- number and range inputs are bound to numeric values -->
|
||||||
|
<input bind:value={a} type=number min=0 max=10>
|
||||||
|
<input bind:value={b} type=range min=0 max=10>
|
||||||
|
|
||||||
|
<p>{a} * {b} = {a * b}</p>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
input {
|
||||||
|
display: block;
|
||||||
|
width: 10em
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"a": 5,
|
||||||
|
"b": 5
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
<label>
|
||||||
|
<input type=radio bind:group={selected} value="red">
|
||||||
|
red
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type=radio bind:group={selected} value="green">
|
||||||
|
green
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type=radio bind:group={selected} value="blue">
|
||||||
|
blue
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p style="color: {selected};">selected {selected}</p>
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"selected": "blue"
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
<input bind:value={name} placeholder="enter your name">
|
||||||
|
<p>Hello {name || 'stranger'}!</p>
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"name": ""
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
<script>
|
||||||
|
export let paused = true;
|
||||||
|
export let t = 0;
|
||||||
|
export let d;
|
||||||
|
|
||||||
|
let icon, bg;
|
||||||
|
|
||||||
|
$: icon = `https://icon.now.sh/${paused ? 'play' : 'pause'}_circle_filled`;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
var p = d ? t / d : 0;
|
||||||
|
var h = 90 + 90 * p;
|
||||||
|
var l = 10 + p * 30;
|
||||||
|
bg = `hsl(${h},50%,${l}%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(num) {
|
||||||
|
return num < 10 ? '0' + num : num;
|
||||||
|
}
|
||||||
|
|
||||||
|
const format = time => {
|
||||||
|
if (isNaN(time)) return '--:--.-';
|
||||||
|
var minutes = Math.floor(time / 60);
|
||||||
|
var seconds = (time % 60).toFixed(1);
|
||||||
|
|
||||||
|
return minutes + ':' + pad(seconds)
|
||||||
|
};
|
||||||
|
|
||||||
|
function seek(event) {
|
||||||
|
if (event.buttons === 1) {
|
||||||
|
event.preventDefault();
|
||||||
|
var p = event.clientX / window.innerWidth;
|
||||||
|
t = p * d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:click={seek} on:mousemove={seek}/>
|
||||||
|
|
||||||
|
<audio bind:currentTime={t} bind:duration={d} bind:paused>
|
||||||
|
<source type="audio/mp3" src="https://deepnote.surge.sh/deepnote.mp3">
|
||||||
|
</audio>
|
||||||
|
|
||||||
|
<p>THX Deep Note</p>
|
||||||
|
<div class="status" on:click="{event => event.stopPropagation()}">
|
||||||
|
<img alt="play/pause button" on:click="{() => paused = !paused}" src="{icon}/333333">
|
||||||
|
<span class="elapsed">{format(t)}</span>
|
||||||
|
<span class="duration">{format(d)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress" style="width: {d ? 100 * t/d : 0}%; background: {bg};">
|
||||||
|
<p>THX Deep Note</p>
|
||||||
|
<div class="status" on:click="{event => event.stopPropagation()}">
|
||||||
|
<img alt="play/pause button" src="{icon}/ffffff">
|
||||||
|
<span class="elapsed">{format(t)}</span>
|
||||||
|
<span class="duration">{format(d)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.progress {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: white;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
position: absolute;
|
||||||
|
left: 1em;
|
||||||
|
top: 1em;
|
||||||
|
width: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1em;
|
||||||
|
left: 1em;
|
||||||
|
width: calc(100vw - 2em);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 2em;
|
||||||
|
width: 3em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elapsed { float: left; }
|
||||||
|
.duration { float: right; }
|
||||||
|
</style>
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,15 @@
|
|||||||
|
<script>
|
||||||
|
import marked from 'marked';
|
||||||
|
|
||||||
|
export let markdown;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<textarea bind:value={markdown} resize="none"></textarea>
|
||||||
|
<div class="output">{@html marked(markdown)}</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"markdown": "# Markdown editor\n\nTODOs:\n\n* make a Svelte app\n* think of a third item for this list"
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
<h1>Cats of YouTube</h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{#each cats as cat}
|
||||||
|
<li><a target="_blank" href={cat.video}>{cat.name}</a></li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"cats": [
|
||||||
|
{
|
||||||
|
"name": "Keyboard Cat",
|
||||||
|
"video": "https://www.youtube.com/watch?v=J---aiyznGQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Maru",
|
||||||
|
"video": "https://www.youtube.com/watch?v=z_AbfPXTKms"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Henri The Existential Cat",
|
||||||
|
"video": "https://www.youtube.com/watch?v=OUtn3pvWmpg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import List from './List.html';
|
||||||
|
import Item from './Item.html';
|
||||||
|
|
||||||
|
let item;
|
||||||
|
let page;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
hashchange();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function hashchange() {
|
||||||
|
// the poor man's router!
|
||||||
|
const path = window.location.hash.slice(1);
|
||||||
|
|
||||||
|
if (path.startsWith('/item')) {
|
||||||
|
const id = path.slice(6);
|
||||||
|
item = await fetch(`https://node-hnapi.herokuapp.com/item/${id}`).then(r => r.json());
|
||||||
|
|
||||||
|
window.scrollTo(0,0);
|
||||||
|
} else if (path.startsWith('/top')) {
|
||||||
|
page = +path.slice(5);
|
||||||
|
item = null;
|
||||||
|
} else {
|
||||||
|
window.location.hash = '/top/1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
position: relative;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 101vh;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main :global(.meta) {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main :global(a) {
|
||||||
|
color: rgb(0,0,150);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<svelte:window on:hashchange={hashchange}/>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{#if item}
|
||||||
|
<Item {item}/>
|
||||||
|
{:elseif page}
|
||||||
|
<List {page}/>
|
||||||
|
{/if}
|
||||||
|
</main>
|
@ -0,0 +1,28 @@
|
|||||||
|
<style>
|
||||||
|
article {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
margin: 1em 0 0 0;
|
||||||
|
padding: 1em 0 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replies {
|
||||||
|
padding: 0 0 0 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<p class="meta">{comment.user} {comment.time_ago}</p>
|
||||||
|
|
||||||
|
{@html comment.content}
|
||||||
|
|
||||||
|
<div class="replies">
|
||||||
|
{#each comment.comments as child}
|
||||||
|
<svelte:self comment={child}/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</article>
|
@ -0,0 +1,43 @@
|
|||||||
|
<script>
|
||||||
|
import Comment from "./Comment.html";
|
||||||
|
|
||||||
|
export let item;
|
||||||
|
|
||||||
|
function back(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
article {
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.4em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<a href="#/top/1" on:click={back}>« back</a>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<a href="{item.url}">
|
||||||
|
<h1>{item.title}</h1>
|
||||||
|
<small>{item.domain}</small>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<p class="meta">submitted by {item.user} {item.time_ago}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div class="comments">
|
||||||
|
{#each item.comments as comment}
|
||||||
|
<Comment {comment}/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
@ -0,0 +1,49 @@
|
|||||||
|
<script>
|
||||||
|
import { beforeUpdate } from "svelte";
|
||||||
|
import Summary from "./Summary.html";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
|
export let items;
|
||||||
|
export let offset;
|
||||||
|
export let page;
|
||||||
|
|
||||||
|
let previous_page;
|
||||||
|
|
||||||
|
beforeUpdate(async () => {
|
||||||
|
if (page !== previous_page) {
|
||||||
|
previous_page = page;
|
||||||
|
|
||||||
|
items = await fetch(`https://node-hnapi.herokuapp.com/news?page=${page}`).then(r => r.json())
|
||||||
|
offset = PAGE_SIZE * (page - 1);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
padding: 2em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
opacity: 0;
|
||||||
|
animation: 0.4s 0.8s forwards fade-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{#if items}
|
||||||
|
{#each items as item, i}
|
||||||
|
<Summary {item} {i} {offset}/>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<a href="#/top/{page + 1}">page {page + 1}</a>
|
||||||
|
{:else}
|
||||||
|
<p class="loading">loading...</p>
|
||||||
|
{/if}
|
@ -0,0 +1,38 @@
|
|||||||
|
<script>
|
||||||
|
export let item;
|
||||||
|
export let i;
|
||||||
|
export let offset;
|
||||||
|
|
||||||
|
function comment_text() {
|
||||||
|
const c = item.comments_count;
|
||||||
|
return `${c} ${c === 1 ? 'comment' : 'comments'}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
article {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 0 0 2em;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<span>{i + offset + 1}</span>
|
||||||
|
<h2><a target="_blank" href={item.url}>{item.title}</a></h2>
|
||||||
|
<p class="meta"><a href="#/item/{item.id}">{comment_text()}</a> by {item.user} {item.time_ago}</p>
|
||||||
|
</article>
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,14 @@
|
|||||||
|
<h1>Hello {name}!</h1>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This is a Svelte component. Click the toggle
|
||||||
|
below right to see the generated code.
|
||||||
|
|
||||||
|
You can interact with this component via your
|
||||||
|
browser's console - try running the following:
|
||||||
|
|
||||||
|
app.name = 'everybody';
|
||||||
|
|
||||||
|
You can also update the data via the props
|
||||||
|
editor on this page.
|
||||||
|
-->
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"name": "world"
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
{#if foo}
|
||||||
|
<p>foo!</p>
|
||||||
|
{:else}
|
||||||
|
<p>not foo!</p>
|
||||||
|
{/if}
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"foo": true
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
<svelte:meta immutable/>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ImmutableTodo from './ImmutableTodo.html';
|
||||||
|
import MutableTodo from './MutableTodo.html';
|
||||||
|
|
||||||
|
export let todos;
|
||||||
|
|
||||||
|
function toggle(id) {
|
||||||
|
todos = todos.map(todo => {
|
||||||
|
if (todo.id === id) {
|
||||||
|
// return a new object
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
done: !todo.done,
|
||||||
|
text: todo.text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the same object
|
||||||
|
return todo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2>Immutable</h2>
|
||||||
|
{#each todos as todo}
|
||||||
|
<label on:click="{() => toggle(todo.id)}">
|
||||||
|
<span>{todo.done ? "😎": "☹️"}</span>
|
||||||
|
<ImmutableTodo {todo}/>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<h2>Mutable</h2>
|
||||||
|
{#each todos as todo}
|
||||||
|
<label on:click="{() => toggle(todo.id)}">
|
||||||
|
<span>{todo.done ? "😎": "☹️"}</span>
|
||||||
|
<MutableTodo {todo}/>
|
||||||
|
</label>
|
||||||
|
{/each}
|
@ -0,0 +1,15 @@
|
|||||||
|
<svelte:meta immutable/>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { afterUpdate } from 'svelte';
|
||||||
|
import flash from './flash.js';
|
||||||
|
|
||||||
|
export let todo;
|
||||||
|
let span;
|
||||||
|
|
||||||
|
flash(() => span);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- the text will flash red whenever
|
||||||
|
the `todo` object changes -->
|
||||||
|
<span bind:this={span}>{todo.text}</span>
|
@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
import { afterUpdate } from 'svelte';
|
||||||
|
import flash from './flash.js';
|
||||||
|
|
||||||
|
export let todo;
|
||||||
|
let span;
|
||||||
|
|
||||||
|
flash(() => span);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- the text will flash red whenever
|
||||||
|
the `todo` object changes -->
|
||||||
|
<span bind:this={span}>{todo.text}</span>
|
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
todos: [
|
||||||
|
{ id: 1, done: true, text: "wash the car" },
|
||||||
|
{ id: 2, done: false, text: "take the dog for a walk" },
|
||||||
|
{ id: 3, done: false, text: "mow the lawn" }
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { afterUpdate } from 'svelte';
|
||||||
|
|
||||||
|
export default function flash(fn) {
|
||||||
|
afterUpdate(() => {
|
||||||
|
const span = fn();
|
||||||
|
|
||||||
|
span.style.color = 'red';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
span.style.color = 'black';
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { scaleLinear } from 'd3-scale';
|
||||||
|
|
||||||
|
export let points;
|
||||||
|
|
||||||
|
const yTicks = [0, 2, 4, 6, 8];
|
||||||
|
const xTicks = [1980, 1990, 2000, 2010];
|
||||||
|
const padding = { top: 20, right: 15, bottom: 20, left: 25 };
|
||||||
|
|
||||||
|
let svg;
|
||||||
|
let width = 500;
|
||||||
|
let height = 200;
|
||||||
|
|
||||||
|
let xScale;
|
||||||
|
let yScale;
|
||||||
|
let minX;
|
||||||
|
let maxX;
|
||||||
|
let path;
|
||||||
|
let area;
|
||||||
|
|
||||||
|
$: xScale = scaleLinear()
|
||||||
|
.domain([minX, maxX])
|
||||||
|
.range([padding.left, width - padding.right]);
|
||||||
|
|
||||||
|
$: yScale = scaleLinear()
|
||||||
|
.domain([Math.min.apply(null, yTicks), Math.max.apply(null, yTicks)])
|
||||||
|
.range([height - padding.bottom, padding.top]);
|
||||||
|
|
||||||
|
$: minX = points[0].x;
|
||||||
|
$: maxX = points[points.length - 1].x;
|
||||||
|
$: path = `M${points.map(p => `${xScale(p.x)},${yScale(p.y)}`).join('L')}`;
|
||||||
|
$: area = `${path}L${xScale(maxX)},${yScale(0)}L${xScale(minX)},${yScale(0)}Z`;
|
||||||
|
|
||||||
|
function formatMobile (tick) {
|
||||||
|
return "'" + tick % 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
const bcr = svg.getBoundingClientRect();
|
||||||
|
width = bcr.width;
|
||||||
|
height = bcr.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(resize);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:resize='{resize}'/>
|
||||||
|
|
||||||
|
<div class="chart">
|
||||||
|
<h2>Arctic sea ice minimum</h2>
|
||||||
|
|
||||||
|
<svg bind:this={svg}>
|
||||||
|
<!-- y axis -->
|
||||||
|
<g class="axis y-axis" transform="translate(0, {padding.top})">
|
||||||
|
{#each yTicks as tick}
|
||||||
|
<g class="tick tick-{tick}" transform="translate(0, {yScale(tick) - padding.bottom})">
|
||||||
|
<line x2="100%"></line>
|
||||||
|
<text y="-4">{tick} {tick === 8 ? ' million sq km' : ''}</text>
|
||||||
|
</g>
|
||||||
|
{/each}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- x axis -->
|
||||||
|
<g class="axis x-axis">
|
||||||
|
{#each xTicks as tick}
|
||||||
|
<g class="tick tick-{ tick }" transform="translate({xScale(tick)},{height})">
|
||||||
|
<line y1="-{height}" y2="-{padding.bottom}" x1="0" x2="0"></line>
|
||||||
|
<text y="-2">{width > 380 ? tick : formatMobile(tick)}</text>
|
||||||
|
</g>
|
||||||
|
{/each}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- data -->
|
||||||
|
<path class="path-area" d={area}></path>
|
||||||
|
<path class="path-line" d={path}></path>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<p>Average September extent. Source: <a href='https://climate.nasa.gov/vital-signs/arctic-sea-ice/'>NSIDC/NASA</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick {
|
||||||
|
font-size: .725em;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick line {
|
||||||
|
stroke: #aaa;
|
||||||
|
stroke-dasharray: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick text {
|
||||||
|
fill: #666;
|
||||||
|
text-anchor: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick.tick-0 line {
|
||||||
|
stroke-dasharray: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-axis .tick text {
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-line {
|
||||||
|
fill: none;
|
||||||
|
stroke: rgb(0,100,100);
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-area {
|
||||||
|
fill: rgba(0,100,100,0.2);
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,156 @@
|
|||||||
|
{
|
||||||
|
"points": [
|
||||||
|
{
|
||||||
|
"x": 1979,
|
||||||
|
"y": 7.19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1980,
|
||||||
|
"y": 7.83
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1981,
|
||||||
|
"y": 7.24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1982,
|
||||||
|
"y": 7.44
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1983,
|
||||||
|
"y": 7.51
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1984,
|
||||||
|
"y": 7.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1985,
|
||||||
|
"y": 6.91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1986,
|
||||||
|
"y": 7.53
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1987,
|
||||||
|
"y": 7.47
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1988,
|
||||||
|
"y": 7.48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1989,
|
||||||
|
"y": 7.03
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1990,
|
||||||
|
"y": 6.23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1991,
|
||||||
|
"y": 6.54
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1992,
|
||||||
|
"y": 7.54
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1993,
|
||||||
|
"y": 6.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1994,
|
||||||
|
"y": 7.18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1995,
|
||||||
|
"y": 6.12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1996,
|
||||||
|
"y": 7.87
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1997,
|
||||||
|
"y": 6.73
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1998,
|
||||||
|
"y": 6.55
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1999,
|
||||||
|
"y": 6.23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2000,
|
||||||
|
"y": 6.31
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2001,
|
||||||
|
"y": 6.74
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2002,
|
||||||
|
"y": 5.95
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2003,
|
||||||
|
"y": 6.13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2004,
|
||||||
|
"y": 6.04
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2005,
|
||||||
|
"y": 5.56
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2006,
|
||||||
|
"y": 5.91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2007,
|
||||||
|
"y": 4.29
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2008,
|
||||||
|
"y": 4.72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2009,
|
||||||
|
"y": 5.38
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2010,
|
||||||
|
"y": 4.92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2011,
|
||||||
|
"y": 4.61
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2012,
|
||||||
|
"y": 3.62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2013,
|
||||||
|
"y": 5.35
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2014,
|
||||||
|
"y": 5.28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2015,
|
||||||
|
"y": 4.63
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 2016,
|
||||||
|
"y": 4.72
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,175 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Basics",
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"slug": "hello-world",
|
||||||
|
"title": "Hello World!"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "if-blocks",
|
||||||
|
"title": "If blocks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "each-blocks",
|
||||||
|
"title": "Each blocks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "scoped-styles",
|
||||||
|
"title": "Scoped styles"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Two-way bindings",
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"slug": "binding-input-text",
|
||||||
|
"title": "Text input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "binding-input-numeric",
|
||||||
|
"title": "Numeric input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "binding-textarea",
|
||||||
|
"title": "Textarea"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "binding-input-checkbox",
|
||||||
|
"title": "Checkbox input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "binding-input-checkbox-group",
|
||||||
|
"title": "Checkbox input (grouped)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "binding-input-radio",
|
||||||
|
"title": "Radio input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "binding-media-elements",
|
||||||
|
"title": "Media elements"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nested components",
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"slug": "nested-components",
|
||||||
|
"title": "Nested components"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "modal-with-slot",
|
||||||
|
"title": "Modal with <slot>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "self-references",
|
||||||
|
"title": "Self-references"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SVG and dataviz",
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"slug": "svg-clock",
|
||||||
|
"title": "SVG Clock"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "line-chart",
|
||||||
|
"title": "Line/area chart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "bar-chart",
|
||||||
|
"title": "Bar chart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "scatterplot",
|
||||||
|
"title": "Scatterplot"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Transitions",
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"slug": "transitions-fade",
|
||||||
|
"title": "Simple fade"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "transitions-fly",
|
||||||
|
"title": "Parameterised"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "transitions-in-out",
|
||||||
|
"title": "In and out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "transitions-custom",
|
||||||
|
"title": "Custom CSS"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Async data",
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"slug": "await-block",
|
||||||
|
"title": "Await block"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "7guis",
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"slug": "7guis-counter",
|
||||||
|
"title": "Counter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "7guis-temperature",
|
||||||
|
"title": "Temperature converter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "7guis-flight-booker",
|
||||||
|
"title": "Flight booker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "7guis-timer",
|
||||||
|
"title": "Timer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "7guis-crud",
|
||||||
|
"title": "CRUD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "7guis-circles",
|
||||||
|
"title": "Circles"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "<:Window>",
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"slug": "parallax",
|
||||||
|
"title": "Parallax"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Miscellaneous",
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"slug": "hacker-news",
|
||||||
|
"title": "Hacker News"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "immutable",
|
||||||
|
"title": "Immutable data"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,45 @@
|
|||||||
|
<script>
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='modal-background' on:click='{() => dispatch("close")}'></div>
|
||||||
|
|
||||||
|
<div class='modal'>
|
||||||
|
<slot name='header'></slot>
|
||||||
|
<hr>
|
||||||
|
<slot></slot>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<button on:click='{() => dispatch("close")}'>close modal</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.modal-background {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: calc(100vw - 4em);
|
||||||
|
max-width: 32em;
|
||||||
|
max-height: calc(100vh - 4em);
|
||||||
|
overflow: auto;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
padding: 1em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"showModal": true
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
import Nested from './Nested.html';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>This is a top-level element.</p>
|
||||||
|
<Nested/>
|
@ -0,0 +1 @@
|
|||||||
|
<p>And this is a nested component.</p>
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,76 @@
|
|||||||
|
<script>
|
||||||
|
let sy;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- this binds `sy` to the current value of `window.scrollY` -->
|
||||||
|
<svelte:window bind:scrollY={sy}/>
|
||||||
|
|
||||||
|
<!-- try changing the values that `sy` is multiplied by -
|
||||||
|
values closer to 0 appear further away -->
|
||||||
|
<div class="parallax-container">
|
||||||
|
<img style="transform: translate(0,{-sy * 0.2}px)" src="http://www.firewatchgame.com/images/parallax/parallax0.png">
|
||||||
|
<img style="transform: translate(0,{-sy * 0.3}px)" src="http://www.firewatchgame.com/images/parallax/parallax1.png">
|
||||||
|
<img style="transform: translate(0,{-sy * 0.4}px)" src="http://www.firewatchgame.com/images/parallax/parallax3.png">
|
||||||
|
<img style="transform: translate(0,{-sy * 0.5}px)" src="http://www.firewatchgame.com/images/parallax/parallax5.png">
|
||||||
|
<img style="transform: translate(0,{-sy * 0.6}px)" src="http://www.firewatchgame.com/images/parallax/parallax7.png">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text">
|
||||||
|
<small style="
|
||||||
|
transform: translate(0,{-sy * 1.5}px);
|
||||||
|
opacity: {1 - Math.max( 0, sy / 80 )}
|
||||||
|
">(scroll down)</small>
|
||||||
|
<span>parallax has never been this easy</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.parallax-container {
|
||||||
|
position: fixed;
|
||||||
|
width: 2400px;
|
||||||
|
height: 712px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%,0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.parallax-container img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 50vh 0.5em 0.5em 0.5em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: linear-gradient(to bottom, rgba(45,10,13,0) 60vh,rgba(45,10,13,1) 712px);
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
display: block;
|
||||||
|
font-size: 4vw;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text span {
|
||||||
|
font-size: 20vw;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body) { margin: 0; padding: 0; }
|
||||||
|
</style>
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,28 @@
|
|||||||
|
<script>
|
||||||
|
import Scatterplot from './Scatterplot.html';
|
||||||
|
|
||||||
|
export let a;
|
||||||
|
export let b;
|
||||||
|
export let c;
|
||||||
|
export let d;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="chart">
|
||||||
|
<h2>Anscombe's quartet</h2>
|
||||||
|
|
||||||
|
<Scatterplot points={a}/>
|
||||||
|
<Scatterplot points={b}/>
|
||||||
|
<Scatterplot points={c}/>
|
||||||
|
<Scatterplot points={d}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640px;
|
||||||
|
height: calc(100% - 4em);
|
||||||
|
min-height: 280px;
|
||||||
|
max-height: 480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,111 @@
|
|||||||
|
<script>
|
||||||
|
// TODO this example needs updating
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { scaleLinear } from 'd3-scale';
|
||||||
|
|
||||||
|
export let svg;
|
||||||
|
|
||||||
|
const xScale = scaleLinear();
|
||||||
|
const yScale = scaleLinear();
|
||||||
|
|
||||||
|
export let width = 500;
|
||||||
|
export let height = 200;
|
||||||
|
export let padding = { top: 20, right: 40, bottom: 40, left: 25 };
|
||||||
|
export let points;
|
||||||
|
export let xTicks = [0, 4, 8, 12, 16, 20];
|
||||||
|
export let yTicks = [0, 2, 4, 6, 8, 10, 12];
|
||||||
|
|
||||||
|
function xTicks() {
|
||||||
|
return width > 180 ?
|
||||||
|
[0, 4, 8, 12, 16, 20] :
|
||||||
|
[0, 10, 20];
|
||||||
|
}
|
||||||
|
|
||||||
|
function yTicks() {
|
||||||
|
return height > 180 ?
|
||||||
|
[0, 2, 4, 6, 8, 10, 12] :
|
||||||
|
[0, 4, 8, 12];
|
||||||
|
}
|
||||||
|
|
||||||
|
function xScale() {
|
||||||
|
return xScale()
|
||||||
|
.domain([0, 20])
|
||||||
|
.range([padding.left, width - padding.right]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function yScale() {
|
||||||
|
return yScale()
|
||||||
|
.domain([0, 12])
|
||||||
|
.range([height - padding.bottom, padding.top]);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
resize();
|
||||||
|
});
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
const { width, height } = svg.getBoundingClientRect();
|
||||||
|
width = width, height = height;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:resize='{resize}'/>
|
||||||
|
|
||||||
|
<svg ref:svg>
|
||||||
|
<!-- y axis -->
|
||||||
|
<g class='axis y-axis'>
|
||||||
|
{#each yTicks as tick}
|
||||||
|
<g class='tick tick-{tick}' transform='translate(0, {yScale()(tick)})'>
|
||||||
|
<line x1='{padding.left}' x2='{xScale()(22)}'/>
|
||||||
|
<text x='{padding.left - 8}' y='+4'>{tick}</text>
|
||||||
|
</g>
|
||||||
|
{/each}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- x axis -->
|
||||||
|
<g class='axis x-axis'>
|
||||||
|
{#each xTicks as tick}
|
||||||
|
<g class='tick' transform='translate({xScale()(tick)},0)'>
|
||||||
|
<line y1='{yScale()(0)}' y2='{yScale()(13)}'/>
|
||||||
|
<text y='{height - padding.bottom + 16}'>{tick}</text>
|
||||||
|
</g>
|
||||||
|
{/each}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- data -->
|
||||||
|
{#each points as point}
|
||||||
|
<circle cx='{xScale()(point.x)}' cy='{yScale()(point.y)}' r='5'/>
|
||||||
|
{/each}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
svg {
|
||||||
|
width: 50%;
|
||||||
|
height: 50%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
circle {
|
||||||
|
fill: orange;
|
||||||
|
fill-opacity: 0.6;
|
||||||
|
stroke: rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tick line {
|
||||||
|
stroke: #ddd;
|
||||||
|
stroke-dasharray: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 12px;
|
||||||
|
fill: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-axis text {
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.y-axis text {
|
||||||
|
text-anchor: end;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,186 @@
|
|||||||
|
{
|
||||||
|
"a": [
|
||||||
|
{
|
||||||
|
"x": 10,
|
||||||
|
"y": 8.04
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 6.95
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 13,
|
||||||
|
"y": 7.58
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 9,
|
||||||
|
"y": 8.81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 11,
|
||||||
|
"y": 8.33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 14,
|
||||||
|
"y": 9.96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 6,
|
||||||
|
"y": 7.24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 4,
|
||||||
|
"y": 4.26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 12,
|
||||||
|
"y": 10.84
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 7,
|
||||||
|
"y": 4.82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 5,
|
||||||
|
"y": 5.68
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"b": [
|
||||||
|
{
|
||||||
|
"x": 10,
|
||||||
|
"y": 9.14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 8.14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 13,
|
||||||
|
"y": 8.74
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 9,
|
||||||
|
"y": 8.77
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 11,
|
||||||
|
"y": 9.26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 14,
|
||||||
|
"y": 8.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 6,
|
||||||
|
"y": 6.13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 4,
|
||||||
|
"y": 3.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 12,
|
||||||
|
"y": 9.13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 7,
|
||||||
|
"y": 7.26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 5,
|
||||||
|
"y": 4.74
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"c": [
|
||||||
|
{
|
||||||
|
"x": 10,
|
||||||
|
"y": 7.46
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 6.77
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 13,
|
||||||
|
"y": 12.74
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 9,
|
||||||
|
"y": 7.11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 11,
|
||||||
|
"y": 7.81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 14,
|
||||||
|
"y": 8.84
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 6,
|
||||||
|
"y": 6.08
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 4,
|
||||||
|
"y": 5.39
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 12,
|
||||||
|
"y": 8.15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 7,
|
||||||
|
"y": 6.42
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 5,
|
||||||
|
"y": 5.73
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"d": [
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 6.58
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 5.76
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 7.71
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 8.84
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 8.47
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 7.04
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 5.25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 19,
|
||||||
|
"y": 12.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 5.56
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 7.91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 8,
|
||||||
|
"y": 6.89
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
<div class="foo">
|
||||||
|
Big red Comic Sans
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.foo {
|
||||||
|
color: red;
|
||||||
|
font-size: 2em;
|
||||||
|
font-family: 'Comic Sans MS';
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,9 @@
|
|||||||
|
<ul>
|
||||||
|
<li>{node.name}
|
||||||
|
{#if node.children}
|
||||||
|
{#each node.children as child}
|
||||||
|
<svelte:self node={child}/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
</ul>
|
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"node": {
|
||||||
|
"name": "Fruit",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Red",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Cherry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Strawberry"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Green",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Apple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pear"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lime"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
export let time = new Date();
|
||||||
|
let hours, minutes, seconds;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// this block runs reactively, whenever
|
||||||
|
// `time` changes
|
||||||
|
hours = time.getHours();
|
||||||
|
minutes = time.getMinutes();
|
||||||
|
seconds = time.getSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
time = new Date();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg viewBox='-50 -50 100 100'>
|
||||||
|
<circle class='clock-face' r='48'/>
|
||||||
|
|
||||||
|
<!-- markers -->
|
||||||
|
{#each [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55] as minute}
|
||||||
|
<line
|
||||||
|
class='major'
|
||||||
|
y1='35'
|
||||||
|
y2='45'
|
||||||
|
transform='rotate({30 * minute})'
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#each [1, 2, 3, 4] as offset}
|
||||||
|
<line
|
||||||
|
class='minor'
|
||||||
|
y1='42'
|
||||||
|
y2='45'
|
||||||
|
transform='rotate({6 * (minute + offset)})'
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<!-- hour hand -->
|
||||||
|
<line
|
||||||
|
class='hour'
|
||||||
|
y1='2'
|
||||||
|
y2='-20'
|
||||||
|
transform='rotate({30 * hours + minutes / 2})'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- minute hand -->
|
||||||
|
<line
|
||||||
|
class='minute'
|
||||||
|
y1='4'
|
||||||
|
y2='-30'
|
||||||
|
transform='rotate({6 * minutes + seconds / 10})'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- second hand -->
|
||||||
|
<g transform='rotate({6 * seconds})'>
|
||||||
|
<line class='second' y1='10' y2='-38'/>
|
||||||
|
<line class='second-counterweight' y1='10' y2='2'/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.clock-face {
|
||||||
|
stroke: #333;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
.minor {
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 0.5;
|
||||||
|
}
|
||||||
|
.major {
|
||||||
|
stroke: #333;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
.hour {
|
||||||
|
stroke: #333;
|
||||||
|
}
|
||||||
|
.minute {
|
||||||
|
stroke: #666;
|
||||||
|
}
|
||||||
|
.second, .second-counterweight {
|
||||||
|
stroke: rgb(180,0,0);
|
||||||
|
}
|
||||||
|
.second-counterweight {
|
||||||
|
stroke-width: 3;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,46 @@
|
|||||||
|
<script>
|
||||||
|
import * as eases from 'eases-jsnext';
|
||||||
|
import { fade } from 'svelte-transitions';
|
||||||
|
|
||||||
|
export let visible;
|
||||||
|
|
||||||
|
const wheee = (node, params) => {
|
||||||
|
return {
|
||||||
|
duration: params.duration,
|
||||||
|
css: t => {
|
||||||
|
const eased = eases.elasticOut(t);
|
||||||
|
|
||||||
|
return `
|
||||||
|
transform: scale(${eased}) rotate(${eased * 1080}deg);
|
||||||
|
color: hsl(
|
||||||
|
${~~(t * 360)},
|
||||||
|
${Math.min(100, 1000 - 1000 * t)}%,
|
||||||
|
${Math.min(50, 500 - 500 * t)}%
|
||||||
|
);`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type=checkbox bind:checked={visible}> visible
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<div class="centered" in:wheee="{{duration: 8000}}" out:fade>
|
||||||
|
<span>wheeee!!!!!</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.centered {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
font-size: 4em;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,11 @@
|
|||||||
|
<script>
|
||||||
|
import { fade } from 'svelte-transitions';
|
||||||
|
|
||||||
|
export let visible;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type=checkbox bind:checked={visible}> visible
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<p transition:fade>fades in and out</p>
|
||||||
|
{/if}
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,11 @@
|
|||||||
|
<script>
|
||||||
|
import { fly } from 'svelte-transitions';
|
||||||
|
|
||||||
|
export let visible;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type=checkbox bind:checked={visible}> visible
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<p transition:fly="{{y: 200, duration: 1000}}">flies 200 pixels up, slowly</p>
|
||||||
|
{/if}
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,11 @@
|
|||||||
|
<script>
|
||||||
|
import { fade, fly } from 'svelte-transitions';
|
||||||
|
|
||||||
|
export let visible;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type=checkbox bind:checked={visible}> visible
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<p in:fly="{{y: 50}}" out:fade>flies up, fades out</p>
|
||||||
|
{/if}
|
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -0,0 +1,121 @@
|
|||||||
|
---
|
||||||
|
title: Behaviours
|
||||||
|
---
|
||||||
|
|
||||||
|
As well as scoped styles and a template, components can encapsulate *behaviours*. For that, we add a `<script>` element:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Behaviours' } -->
|
||||||
|
<script>
|
||||||
|
// behaviours go here
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!-- template goes here -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Internal state
|
||||||
|
|
||||||
|
Often, it makes sense for a component to have internal state that isn't visible to the outside world.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Internal state' } -->
|
||||||
|
<script>
|
||||||
|
let count = 0;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>Count: {count}</p>
|
||||||
|
<button on:click="{() => count += 1}">+1</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### External properties
|
||||||
|
|
||||||
|
On the other hand, for the component to form part of a system, it needs to expose certain values so that they can be set from outside. These are called *props*, and we use the `export` keyword to differentiate them from internal state:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'External properties' } -->
|
||||||
|
<script>
|
||||||
|
export let count = 0;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>Count: {count}</p>
|
||||||
|
<button on:click="{() => count += 1}">+1</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
> Effectively, we're exporting a *contract* with the outside world. The `export` keyword normally means something different in JavaScript, so you might be surprised to see it used like this. Just roll with it for now!
|
||||||
|
|
||||||
|
The `= 0` sets a default value for `count`, if none is provided.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const counter = new Counter({
|
||||||
|
target: document.body,
|
||||||
|
props: {
|
||||||
|
count: 99
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
counter.count; // 99
|
||||||
|
counter.count += 1; // 100
|
||||||
|
```
|
||||||
|
|
||||||
|
Props declared with `const` or `function` are *read-only* — they cannot be set from outside. This allows you to, for example, attach custom methods to your component:
|
||||||
|
|
||||||
|
```js
|
||||||
|
component.doSomethingFun();
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Lifecycle hooks
|
||||||
|
|
||||||
|
There are four 'hooks' provided by Svelte for adding control logic — `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`. Import them directly from `svelte`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Lifecycle hooks' } -->
|
||||||
|
<script>
|
||||||
|
import { onMount, beforeUpdate, afterUpdate, onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
beforeUpdate(() => {
|
||||||
|
// this function is called immediately before
|
||||||
|
// the component updates to reflect new data
|
||||||
|
console.log(`beforeUpdate`);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
// this function is called immediately *after*
|
||||||
|
// the component updates to reflect new data.
|
||||||
|
// if you need to do anything that assumes the
|
||||||
|
// DOM is up-to-date — such as measuring the
|
||||||
|
// size of an element — do it here
|
||||||
|
console.log(`afterUpdate`);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// this function is called once, after the
|
||||||
|
// `afterUpdate` function (if there is one)
|
||||||
|
// runs for the first time
|
||||||
|
console.log(`onMount`);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// this function runs when the
|
||||||
|
// component is destroyed
|
||||||
|
console.log(`onMount cleanup`);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
// this function runs when the
|
||||||
|
// component is destroyed
|
||||||
|
console.log(`onDestroy`);
|
||||||
|
});
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click="{() => count += 1}">
|
||||||
|
Trigger an update ({count})
|
||||||
|
</button>
|
||||||
|
|
||||||
|
> Lifecycle hooks do *not* run in server-side rendering (SSR) mode, with the exception of `onDestroy`. More on SSR later.
|
@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
title: Nested components
|
||||||
|
---
|
||||||
|
|
||||||
|
As well as containing elements (and `if` blocks and `each` blocks), Svelte components can contain *other* Svelte components.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Nested components' } -->
|
||||||
|
<script>
|
||||||
|
import Widget from './Widget.html';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='widget-container'>
|
||||||
|
<Widget answer={42}/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!--{ filename: 'Widget.html' }-->
|
||||||
|
<p>I am a nested component. The answer is {answer}</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
That's similar to doing this...
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Widget from './Widget.html';
|
||||||
|
|
||||||
|
const widget = new Widget({
|
||||||
|
target: document.querySelector('.widget-container'),
|
||||||
|
props: {
|
||||||
|
answer: 42
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
...except that Svelte takes care of destroying the child component when the parent is destroyed, and keeps props in sync if they change.
|
||||||
|
|
||||||
|
> Component names must be capitalised, following the widely-used JavaScript convention of capitalising constructor names. It's also an easy way to distinguish components from elements in your template.
|
||||||
|
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
Props, short for 'properties', are the means by which you pass data down from a parent to a child component — in other words, they're just like attributes on an element. As with element attributes, prop values can contain any valid JavaScript expression.
|
||||||
|
|
||||||
|
Often, the name of the property will be the same as the value, in which case we can use a shorthand:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { repl: false } -->
|
||||||
|
<!-- these are equivalent -->
|
||||||
|
<Widget foo={foo}/>
|
||||||
|
<Widget {foo}/>
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note that props are *one-way* — to get data from a child component into a parent component, use [bindings](guide#bindings).
|
||||||
|
|
||||||
|
|
||||||
|
### Composing with `<slot>`
|
||||||
|
|
||||||
|
A component can contain a `<slot></slot>` element, which allows the parent component to inject content:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Using <slot>' } -->
|
||||||
|
<script>
|
||||||
|
import Box from './Box.html';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<h2>Hello!</h2>
|
||||||
|
<p>This is a box. It can contain anything.</p>
|
||||||
|
</Box>
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!--{ filename: 'Box.html' }-->
|
||||||
|
<style>
|
||||||
|
.box {
|
||||||
|
border: 2px solid black;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<slot><!-- content is injected here --></slot>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
The `<slot>` element can contain 'fallback content', which will be used if no children are provided for the component:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Default slot content' } -->
|
||||||
|
<script>
|
||||||
|
import Box from './Box.html';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Box></Box>
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!--{ filename: 'Box.html' }-->
|
||||||
|
<style>
|
||||||
|
.box {
|
||||||
|
border: 2px solid black;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<slot>
|
||||||
|
<p class="fallback">the box is empty!</p>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also have *named* slots. Any elements with a corresponding `slot` attribute will fill these slots:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Named slots' } -->
|
||||||
|
<script>
|
||||||
|
import ContactCard from './ContactCard.html';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ContactCard>
|
||||||
|
<span slot="name">P. Sherman</span>
|
||||||
|
<span slot="address">42 Wallaby Way, Sydney</span>
|
||||||
|
</ContactCard>
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!--{ filename: 'ContactCard.html' }-->
|
||||||
|
<style>
|
||||||
|
.contact-card {
|
||||||
|
border: 2px solid black;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="contact-card">
|
||||||
|
<h2><slot name="name"></slot></h2>
|
||||||
|
<slot name="address">Unknown address</slot>
|
||||||
|
<br>
|
||||||
|
<slot name="email">Unknown email</slot>
|
||||||
|
</div>
|
||||||
|
```
|
@ -0,0 +1,180 @@
|
|||||||
|
---
|
||||||
|
title: Events
|
||||||
|
---
|
||||||
|
|
||||||
|
In most applications, you'll need to respond to the user's actions. In Svelte, this is done with the `on:[event]` directive.
|
||||||
|
|
||||||
|
### Element events
|
||||||
|
|
||||||
|
When used on an element, `on:click={handler}` is equivalent to calling `element.addEventListener('click', handler)`. When the element is removed, Svelte calls `removeEventListener` automatically.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Inline event handlers' } -->
|
||||||
|
<p>Count: {count}</p>
|
||||||
|
<button on:click="{() => count += 1}">+1</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
/* { hidden: true } */
|
||||||
|
{
|
||||||
|
count: 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For more complicated behaviours, you'll probably want to declare an event handler in your `<script>` block:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Event handlers' } -->
|
||||||
|
<script>
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
function incrementOrDecrement(event) {
|
||||||
|
const d = event.shiftKey
|
||||||
|
? -1
|
||||||
|
: +1;
|
||||||
|
|
||||||
|
count += d;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>Count: {count}</p>
|
||||||
|
<button on:click={incrementOrDecrement}>update</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
/* { hidden: true } */
|
||||||
|
{
|
||||||
|
count: 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Event handler modifiers
|
||||||
|
|
||||||
|
While you can invoke methods like `event.stopPropagation` directly...
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { repl: false } -->
|
||||||
|
<div on:click="{e => e.stopPropagation()}">...</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
...it gets annoying if you want to combine that with some other behaviour:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { repl: false } -->
|
||||||
|
<script>
|
||||||
|
let foo = false;
|
||||||
|
|
||||||
|
function toggleFoo(event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
foo = !foo;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div on:click={toggleFoo}>...</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
For that reason, Svelte lets you use *event modifiers*:
|
||||||
|
|
||||||
|
- [`preventDefault`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault)
|
||||||
|
- [`stopPropagation`](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation)
|
||||||
|
- [`passive`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters) — improves scrolling performance on touch/wheel events (Svelte will add it automatically where it's safe to do so)
|
||||||
|
- [`once`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters) — removes the listener after the first invocation
|
||||||
|
- [`capture`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameter)
|
||||||
|
|
||||||
|
> `passive` and `once` are not implemented in `legacy` mode
|
||||||
|
|
||||||
|
The example above can be achieved with modifiers — no need for a separate event handler:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { repl: false } -->
|
||||||
|
<div on:click|stopPropagation|preventDefault="{() => foo = !foo}">...</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Component events
|
||||||
|
|
||||||
|
Events are an excellent way for [nested components](guide#nested-components) to communicate with their parents. Let's revisit our earlier example, but turn it into a `<CategoryChooser>` component:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { filename: 'CategoryChooser.html', repl: false } -->
|
||||||
|
<p>Select a category:</p>
|
||||||
|
|
||||||
|
{#each categories as category}
|
||||||
|
<button on:click="fire('select', { category })">select {category}</button>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
categories: [
|
||||||
|
'animal',
|
||||||
|
'vegetable',
|
||||||
|
'mineral'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
When the user clicks a button, the component will fire a `select` event, where the `event` object has a `category` property. Any component that nests `<CategoryChooser>` can listen for events like so:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!--{ title: 'Component events' }-->
|
||||||
|
<CategoryChooser on:select="playTwentyQuestions(event.category)"/>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CategoryChooser from './CategoryChooser.html';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CategoryChooser
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
playTwentyQuestions(category) {
|
||||||
|
alert(`ok! you chose ${category}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!--{ filename: 'CategoryChooser.html', hidden: true }-->
|
||||||
|
<p>Select a category:</p>
|
||||||
|
|
||||||
|
{#each categories as category}
|
||||||
|
<button on:click="fire('select', { category })">select {category}</button>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
categories: [
|
||||||
|
'animal',
|
||||||
|
'vegetable',
|
||||||
|
'mineral'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Just as `this` in an element's event handler refers to the element itself, in a component event handler `this` refers to the component firing the event.
|
||||||
|
|
||||||
|
There is also a shorthand for listening for and re-firing an event unchanged.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { repl: false } -->
|
||||||
|
<!-- these are equivalent -->
|
||||||
|
<Widget on:foo="fire('foo', event)"/>
|
||||||
|
<Widget on:foo/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Since component events do not propagate as DOM events do, this can be used to pass events through intermediate components. This shorthand technique also applies to element events (`on:click` is equivalent to `on:click="fire('click', event)"`).
|
@ -0,0 +1,161 @@
|
|||||||
|
---
|
||||||
|
title: Bindings
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### Bindings
|
||||||
|
|
||||||
|
As we've seen, data can be passed down to elements and components with attributes and [props](guide#props). Occasionally, you need to get data back *up*; for that we use bindings.
|
||||||
|
|
||||||
|
|
||||||
|
#### Component bindings
|
||||||
|
|
||||||
|
Component bindings keep values in sync between a parent and a child:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { repl: false } -->
|
||||||
|
<Widget bind:childValue=parentValue/>
|
||||||
|
```
|
||||||
|
|
||||||
|
Whenever `childValue` changes in the child component, `parentValue` will be updated in the parent component and vice versa.
|
||||||
|
|
||||||
|
If the names are the same, you can shorten the declaration:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { repl: false } -->
|
||||||
|
<Widget bind:value/>
|
||||||
|
```
|
||||||
|
|
||||||
|
> Use component bindings judiciously. They can save you a lot of boilerplate, but will make it harder to reason about data flow within your application if you overuse them.
|
||||||
|
|
||||||
|
|
||||||
|
#### Element bindings
|
||||||
|
|
||||||
|
Element bindings make it easy to respond to user interactions:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Element bindings' } -->
|
||||||
|
<h1>Hello {name}!</h1>
|
||||||
|
<input bind:value=name>
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
/* { hidden: true } */
|
||||||
|
{
|
||||||
|
name: 'world'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Some bindings are *one-way*, meaning that the values are read-only. Most are *two-way* — changing the data programmatically will update the DOM. The following bindings are available:
|
||||||
|
|
||||||
|
| Name | Applies to | Kind |
|
||||||
|
|-----------------------------------------------------------------|----------------------------------------------|----------------------|
|
||||||
|
| `value` | `<input>` `<textarea>` `<select>` | <span>Two-way</span> |
|
||||||
|
| `checked` `indeterminate` | `<input type=checkbox>` | <span>Two-way</span> |
|
||||||
|
| `group` (see note) | `<input type=checkbox>` `<input type=radio>` | <span>Two-way</span> |
|
||||||
|
| `currentTime` `paused` `played` `volume` | `<audio>` `<video>` | <span>Two-way</span> |
|
||||||
|
| `buffered` `duration` `seekable` | `<audio>` `<video>` | <span>One-way</span> |
|
||||||
|
| `offsetWidth` `offsetHeight` `clientWidth` `clientHeight` | All block-level elements | <span>One-way</span> |
|
||||||
|
| `scrollX` `scrollY` | `<svelte:window>` | <span>Two-way</span> |
|
||||||
|
| `online` `innerWidth` `innerHeight` `outerWidth` `outerHeight` | `<svelte:window>` | <span>One-way</span> |
|
||||||
|
|
||||||
|
> 'group' bindings allow you to capture the current value of a [set of radio inputs](repl?demo=binding-input-radio), or all the selected values of a [set of checkbox inputs](repl?demo=binding-input-checkbox-group).
|
||||||
|
|
||||||
|
Here is a complete example of using two way bindings with a form:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Form bindings' } -->
|
||||||
|
<form on:submit="handleSubmit(event)">
|
||||||
|
<input bind:value=name type=text>
|
||||||
|
<button type=submit>Say hello</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
handleSubmit(event) {
|
||||||
|
// prevent the page from reloading
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const { name } = this.get();
|
||||||
|
alert(`Hello ${name}!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
/* { hidden: true } */
|
||||||
|
{
|
||||||
|
name: "world"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 'two way' bindings allow you to update a value in a nested property as seen in [checkbox input](repl?demo=binding-input-checkbox).
|
||||||
|
|
||||||
|
|
||||||
|
### bind:this
|
||||||
|
|
||||||
|
There's a special binding that exists on all elements and components — `this`. It allows you to store a reference to a DOM node or component instance so that you can interact with it programmatically:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Refs' } -->
|
||||||
|
<canvas bind:this={canvas} width={200} height={200}></canvas>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import createRenderer from './createRenderer.js';
|
||||||
|
|
||||||
|
let canvas;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const renderer = createRenderer(canvas, ctx);
|
||||||
|
|
||||||
|
// stop updating the canvas when
|
||||||
|
// the component is destroyed
|
||||||
|
return renderer.stop;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
/* { filename: 'createRenderer.js', hidden: true } */
|
||||||
|
export default function createRenderer(canvas, ctx) {
|
||||||
|
let running = true;
|
||||||
|
loop();
|
||||||
|
|
||||||
|
return {
|
||||||
|
stop: () => {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function loop() {
|
||||||
|
if (!running) return;
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
for (let p = 0; p < imageData.data.length; p += 4) {
|
||||||
|
const i = p / 4;
|
||||||
|
const x = i % canvas.width;
|
||||||
|
const y = i / canvas.height >>> 0;
|
||||||
|
|
||||||
|
const t = window.performance.now();
|
||||||
|
|
||||||
|
const r = 64 + (128 * x / canvas.width) + (64 * Math.sin(t / 1000));
|
||||||
|
const g = 64 + (128 * y / canvas.height) + (64 * Math.cos(t / 1000));
|
||||||
|
const b = 128;
|
||||||
|
|
||||||
|
imageData.data[p + 0] = r;
|
||||||
|
imageData.data[p + 1] = g;
|
||||||
|
imageData.data[p + 2] = b;
|
||||||
|
imageData.data[p + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
title: Transitions
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### Transitions
|
||||||
|
|
||||||
|
Transitions allow elements to enter and leave the DOM gracefully, rather than suddenly appearing and disappearing.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Transitions' } -->
|
||||||
|
<script>
|
||||||
|
import { fade } from 'svelte/transition.js';
|
||||||
|
let visible = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type=checkbox bind:checked={visible}> visible
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<p transition:fade>fades in and out</p>
|
||||||
|
{/if}
|
||||||
|
```
|
||||||
|
|
||||||
|
Transitions can have parameters — typically `delay` and `duration`, but often others, depending on the transition in question. For example, here's the `fly` transition from the [svelte-transitions](https://github.com/sveltejs/svelte-transitions) package:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Transition with parameters' } -->
|
||||||
|
<script>
|
||||||
|
import { fly } from 'svelte-transitions';
|
||||||
|
let visible = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type=checkbox bind:checked={visible}> visible
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<p transition:fly="{{y: 200, duration: 1000}}">flies 200 pixels up, slowly</p>
|
||||||
|
{/if}
|
||||||
|
```
|
||||||
|
|
||||||
|
An element can have separate `in` and `out` transitions:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Transition in/out' } -->
|
||||||
|
<script>
|
||||||
|
import { fade, fly } from 'svelte-transitions';
|
||||||
|
let visible = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type=checkbox bind:checked={visible}> visible
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<p in:fly="{y: 50}" out:fade>flies up, fades out</p>
|
||||||
|
{/if}
|
||||||
|
```
|
||||||
|
|
||||||
|
Transitions are simple functions that take a `node` and any provided `parameters` and return an object with the following properties:
|
||||||
|
|
||||||
|
* `duration` — how long the transition takes in milliseconds
|
||||||
|
* `delay` — milliseconds before the transition starts
|
||||||
|
* `easing` — an [easing function](https://github.com/rollup/eases-jsnext)
|
||||||
|
* `css` — a function that accepts an argument `t` between 0 and 1 and returns the styles that should be applied at that moment
|
||||||
|
* `tick` — a function that will be called on every frame, with the same `t` argument, while the transition is in progress
|
||||||
|
|
||||||
|
Of these, `duration` is required, as is *either* `css` or `tick`. The rest are optional. Here's how the `fade` transition is implemented, for example:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Fade transition' } -->
|
||||||
|
<script>
|
||||||
|
function fade(node, { delay = 0, duration = 400 }) {
|
||||||
|
const o = +getComputedStyle(node).opacity;
|
||||||
|
|
||||||
|
return {
|
||||||
|
delay,
|
||||||
|
duration,
|
||||||
|
css: t => `opacity: ${t * o}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let visible = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type=checkbox bind:checked={visible}> visible
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<p transition:fade>fades in and out</p>
|
||||||
|
{/if}
|
||||||
|
```
|
||||||
|
|
||||||
|
> If the `css` option is used, Svelte will create a CSS animation that runs efficiently off the main thread. Therefore if you can achieve an effect using `css` rather than `tick`, you should.
|
@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
title: Actions
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
Actions let you decorate elements with additional functionality. Actions are functions which may return an object with lifecycle methods, `update` and `destroy`. The action will be called when its element is added to the DOM.
|
||||||
|
|
||||||
|
Use actions for things like:
|
||||||
|
|
||||||
|
* tooltips
|
||||||
|
* lazy loading images as the page is scrolled, e.g. `<img use:lazyload data-src='giant-photo.jpg'/>`
|
||||||
|
* capturing link clicks for your client router
|
||||||
|
* adding drag and drop
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Actions' } -->
|
||||||
|
<button on:click={toggleLanguage} use:tooltip={translations[language].tooltip}>
|
||||||
|
{language}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
actions: {
|
||||||
|
tooltip(node, text) {
|
||||||
|
const tooltip = document.createElement('div');
|
||||||
|
tooltip.textContent = text;
|
||||||
|
|
||||||
|
Object.assign(tooltip.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
background: 'black',
|
||||||
|
color: 'white',
|
||||||
|
padding: '0.5em 1em',
|
||||||
|
fontSize: '12px',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
transform: 'translate(5px, -50%)',
|
||||||
|
borderRadius: '2px',
|
||||||
|
transition: 'opacity 0.4s'
|
||||||
|
});
|
||||||
|
|
||||||
|
function position() {
|
||||||
|
const { top, right, bottom } = node.getBoundingClientRect();
|
||||||
|
tooltip.style.top = `${(top + bottom) / 2}px`;
|
||||||
|
tooltip.style.left = `${right}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function append() {
|
||||||
|
document.body.appendChild(tooltip);
|
||||||
|
tooltip.style.opacity = 0;
|
||||||
|
setTimeout(() => tooltip.style.opacity = 1);
|
||||||
|
position();
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove() {
|
||||||
|
tooltip.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener('mouseenter', append);
|
||||||
|
node.addEventListener('mouseleave', remove);
|
||||||
|
|
||||||
|
return {
|
||||||
|
update(text) {
|
||||||
|
tooltip.textContent = text;
|
||||||
|
position();
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
tooltip.remove();
|
||||||
|
node.removeEventListener('mouseenter', append);
|
||||||
|
node.removeEventListener('mouseleave', remove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toggleLanguage() {
|
||||||
|
const { language } = this.get();
|
||||||
|
|
||||||
|
this.set({
|
||||||
|
language: language === 'english' ? 'latin' : 'english'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
/* { hidden: true } */
|
||||||
|
{
|
||||||
|
language: "english",
|
||||||
|
translations: {
|
||||||
|
english: {
|
||||||
|
tooltip: "Switch Languages",
|
||||||
|
},
|
||||||
|
latin: {
|
||||||
|
tooltip: "Itchsway Anguageslay",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
title: Classes
|
||||||
|
---
|
||||||
|
|
||||||
|
Like any attribute, the `class` attribute can be set using regular JavaScript. Suppose we had an `active` class that we wanted to apply to an element when `isActive` is true — we could do it like this:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Dynamic classes using ternaries' } -->
|
||||||
|
<script>
|
||||||
|
let isActive = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.active {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h1 class="{isActive ? 'active' : ''}">red if active</h1>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type=checkbox bind:checked={isActive}> isActive
|
||||||
|
</label>
|
||||||
|
```
|
||||||
|
|
||||||
|
That's a little verbose though, so the `class:` directive gives you a simpler way to achieve the same thing:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Dynamic classes using directives' } -->
|
||||||
|
<script>
|
||||||
|
let isActive = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.active {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
-<h1 class="{isActive ? 'active' : ''}">red if active</h1>
|
||||||
|
+<h1 class:active={isActive}>red if active</h1>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type=checkbox bind:checked={isActive}> isActive
|
||||||
|
</label>
|
||||||
|
```
|
||||||
|
|
||||||
|
As with any directive, you can use any JavaScript expression. If it's a variable name that matches the class name, you can use a shorthand:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Dynamic classes using directives' } -->
|
||||||
|
<script>
|
||||||
|
- let isActive = false;
|
||||||
|
+ let active = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.active {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
-<h1 class:active={isActive}>red if active</h1>
|
||||||
|
+<h1 class:active>red if active</h1>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
- <input type=checkbox bind:checked={isActive}> isActive
|
||||||
|
+ <input type=checkbox bind:checked={active}> active
|
||||||
|
</label>
|
||||||
|
```
|
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
title: Module context
|
||||||
|
---
|
||||||
|
|
||||||
|
So far, our `<script>` tags have been running in the context of a component *instance*. In other words, if you have two components like this...
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Counter' } -->
|
||||||
|
<script>
|
||||||
|
import Counter from './Counter.html';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Counter/>
|
||||||
|
<Counter/>
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!--{ filename: 'Counter.html' }-->
|
||||||
|
<script>
|
||||||
|
let count = 0;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button on:click="{() => count += 1}">+1</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
...each counter has its own `count` variable. The code runs once per instance.
|
||||||
|
|
||||||
|
Occasionally, you want code to run once *per module* instead. For that, we use `context="module"`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Module context' } -->
|
||||||
|
<script context="module">
|
||||||
|
console.log(`this will run once`);
|
||||||
|
const answer = 42;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
console.log(`this will run once per instance`);
|
||||||
|
console.log(`we can 'see' module-level variables like ${answer}`);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
> Don't worry about manually hoisting functions from instance context to module context to avoid creating multiple copies of them — Svelte will do that for you
|
||||||
|
|
||||||
|
|
||||||
|
### Module exports
|
||||||
|
|
||||||
|
Any named exports from a `context="module"` script become part of the module's static exports. For example, to define a `preload` function for use with [Sapper](https://sapper.svelte.technology):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- { title: 'Module exports', repl: false } -->
|
||||||
|
<script context="module">
|
||||||
|
export async function preload({ params }) {
|
||||||
|
const res = await this.fetch(`/blog/${params.slug}.json`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
post: await res.json()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
import BlogPost, { preload } from './BlogPost.html';
|
||||||
|
```
|
||||||
|
|
||||||
|
You can only have named exports — no `export default` — because the component *is* the default export.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue