Skip to content

Commit 9686c1f

Browse files
committed
Initial commit
0 parents  commit 9686c1f

File tree

9 files changed

+355
-0
lines changed

9 files changed

+355
-0
lines changed

README.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# `svelte-meteor-data`
2+
3+
This package integrates the [Svelte](https://svelte.dev) UI framework with
4+
Meteor's Tracker system. It makes it easy to write Svelte components which
5+
react automatically to changes in Meteor's data layer.
6+
7+
This package is still experimental. Use at your peril.
8+
9+
## Installation
10+
11+
To add Svelte to your Meteor app, run:
12+
13+
```bash
14+
meteor add svelte:compiler rdb:svelte-meteor-data
15+
meteor npm install --save [email protected]
16+
```
17+
18+
## Usage
19+
20+
Unlike in Blaze, Svelte does not automatically become aware of changes to Meteor
21+
state, even inside `$:` blocks. This package provides some features that enable
22+
Svelte to become aware of such changes.
23+
24+
### Reactive computations with `useTracker`
25+
26+
The `useTracker()` function can be used to expose any reactive computation as a
27+
Svelte store. You need only pass a callable, which will be run the first time
28+
it is used and then every time the computed value changes. The changed value is
29+
automatically made available to Svelte.
30+
31+
For example, this makes the current Meteor user available in a component, and
32+
causes Svelte to update the appropriate element automatically when the current
33+
user changes:
34+
35+
```svelte
36+
<script>
37+
const currentUser = useTracker(() => Meteor.user());
38+
</script>
39+
40+
<h1>Welcome {$currentUser.username}!</h1>
41+
```
42+
43+
You can even mix Meteor reactivity with Svelte reactivity:
44+
45+
```svelte
46+
<script>
47+
let selectedUserId;
48+
49+
$: selectedUser = useTracker(() => selectedUserId);
50+
</script>
51+
52+
<p>Selected {$selectedUser.username}</p>
53+
```
54+
55+
### Cursors
56+
57+
While it's possible to use queries with `useTracker(() => query.fetch())`, this
58+
package supports a more convenient method, directly treating the cursor as a
59+
Svelte store:
60+
61+
```svelte
62+
<script>
63+
export let fruitColor = 'blue';
64+
65+
$: fruits = Fruits.find({color: fruitColor});
66+
</script>
67+
68+
<p>Showing {$fruits.length} {fruitColor}-colored fruits:</p>
69+
<ul>
70+
{#each $fruits as fruit}
71+
<li>{fruit.name}</li>
72+
{/each}
73+
</ul>
74+
```
75+
76+
### Subscriptions
77+
78+
You can safely use `Meteor.subscribe` in your components without worrying about
79+
clean-up. The subscription will be stopped automatically when the component is
80+
destroyed.
81+
82+
As an added feature, you can use a subscription handle in an `{#await}` block:
83+
84+
```svelte
85+
{#await Meteor.subscribe('todos')}
86+
<p>Loading todos…</p>
87+
{:else}
88+
<TodoList />
89+
{/if}
90+
```
91+
92+
### `Tracker.autorun`
93+
94+
It is possible to use `Tracker.autorun()` to have code automatically be re-run
95+
when its Meteor dependencies change. It will stop running when the component is
96+
destroyed. This will work fine for top-level computations that do not depend on
97+
any dynamic Svelte state, such as this example:
98+
99+
```svelte
100+
<script>
101+
let currentUser;
102+
103+
Tracker.autorun(() => {
104+
currentUser = Meteor.user();
105+
});
106+
</script>
107+
```
108+
109+
However, it will not automatically detect changes to Svelte state, nor can I
110+
guarantee that it will work well with `$:`, so I highly recommend the use of
111+
`useTracker` instead.
112+
113+
### ReactiveVar
114+
115+
A Meteor ReactiveVar will work seamlessly as a Svelte store, and can be accessed
116+
and bound like any writable store using the `$` operator:
117+
118+
```svelte
119+
<script>
120+
import { ReactiveVar } from 'meteor/reactive-var';
121+
122+
const store = new ReactiveVar("initial");
123+
</script>
124+
125+
<input type="text" bind:value={$store} />
126+
127+
<p>Value is {$store}</p>
128+
```

autorun.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Makes Tracker.autorun() computations automatically stop when the component is
3+
* destroyed.
4+
*/
5+
6+
import { Tracker } from 'meteor/tracker';
7+
import { current_component } from 'svelte/internal';
8+
9+
_autorun = Tracker.autorun;
10+
Tracker.autorun = function autorun() {
11+
const computation = _autorun.apply(this, arguments);
12+
if (current_component) {
13+
current_component.$$.on_destroy.push(computation.stop.bind(computation));
14+
}
15+
return computation;
16+
};

cursor.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Implements the Svelte store contract for MongoDB cursors.
3+
*/
4+
5+
import { Mongo } from 'meteor/mongo';
6+
7+
Mongo.Cursor.prototype.subscribe = function(set) {
8+
// Set the initial result directly, without going through the callbacks
9+
const mapFn = this._transform
10+
? element => this._transform(this._projectionFn(element))
11+
: element => this._projectionFn(element);
12+
13+
let result = this._getRawObjects({ordered: true}).map(mapFn);
14+
15+
const handle = this.observe({
16+
_suppress_initial: true,
17+
addedAt: (doc, i) => {
18+
result = [...result.slice(0, i), doc, ...result.slice(i)];
19+
set(result);
20+
},
21+
changedAt: (doc, old, i) => {
22+
result = [...result.slice(0, i), doc, ...result.slice(i + 1)];
23+
set(result);
24+
},
25+
removedAt: (old, i) => {
26+
result = [...result.slice(0, i), ...result.slice(i + 1)];
27+
set(result);
28+
},
29+
movedTo: (doc, from, to) => {
30+
result = [...result.slice(0, from), ...result.slice(from + 1)];
31+
result.splice(to, 0, doc);
32+
set(result);
33+
},
34+
});
35+
36+
set(result);
37+
return handle.stop.bind(this);
38+
};

index.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export { default as useTracker } from './use-tracker';
2+
3+
import './subscribe';
4+
5+
if (Package['mongo']) {
6+
import './cursor';
7+
}
8+
9+
if (Package['reactive-var']) {
10+
import './reactive-var';
11+
}
12+
13+
// Import this last, since it overwrites the built-in Tracker.autorun
14+
import './autorun';

package.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Package.describe({
2+
name: 'rdb:svelte-meteor-data',
3+
version: '0.0.1',
4+
summary: 'Reactively track Meteor data inside Svelt components',
5+
git: 'https://github.com/rdb/svelte-meteor-data',
6+
documentation: 'README.md'
7+
});
8+
9+
Package.onUse(function(api) {
10+
api.versionsFrom('1.8');
11+
api.use('ecmascript');
12+
api.use('tracker');
13+
api.use('svelte:[email protected]_1');
14+
api.use('reactive-var', {weak: true});
15+
api.use('mongo', {weak: true});
16+
api.mainModule('index.js');
17+
});
18+
19+
Package.onTest(function(api) {
20+
api.use('ecmascript');
21+
api.use('tinytest');
22+
api.use('rdb:svelte-meteor-data');
23+
api.use('reactive-var');
24+
api.mainModule('reactive-var.tests.js');
25+
});

reactive-var.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Makes ReactiveVar behave as a Svelte store.
3+
*/
4+
5+
import { ReactiveVar } from 'meteor/reactive-var';
6+
7+
let nextId = 1;
8+
9+
ReactiveVar.prototype.subscribe = function subscribe(set) {
10+
const value = this.curValue;
11+
if (value !== undefined) {
12+
set(value);
13+
}
14+
const id = `svelte-${nextId++}`;
15+
this.dep._dependentsById[id] = {
16+
_id: id,
17+
invalidate: () => {
18+
set(this.curValue);
19+
},
20+
};
21+
return () => {
22+
delete this.dep._dependentsById[id];
23+
};
24+
};

reactive-var.tests.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Tinytest } from "meteor/tinytest";
2+
import { ReactiveVar } from 'meteor/reactive-var';
3+
4+
import './reactive-var';
5+
6+
7+
Tinytest.add('ReactiveVar store contract', function (test) {
8+
const rvar = new ReactiveVar("initial");
9+
10+
let setterCalled = 0;
11+
let setterCalledWith;
12+
13+
function setter(value) {
14+
setterCalled += 1;
15+
setterCalledWith = value;
16+
}
17+
18+
const unsub = rvar.subscribe(setter);
19+
test.equal(setterCalled, 1, 'Subscribe should have called setter once');
20+
test.equal(setterCalledWith, "initial", 'Subscribe should have set initial value');
21+
22+
rvar.set("initial");
23+
test.equal(setterCalled, 1, 'Setter should not be called if value is not changed');
24+
25+
rvar.get();
26+
test.equal(setterCalled, 1, 'Setter should not be called on ReactiveVar.get()');
27+
28+
rvar.set("new");
29+
test.equal(setterCalled, 2, 'Setter should be called if value is changed');
30+
test.equal(setterCalledWith, "new", 'Setter should be called with new value');
31+
32+
unsub();
33+
34+
test.equal(setterCalled, 2, 'Unsubscribe should not call setter');
35+
36+
rvar.set("newer");
37+
test.equal(setterCalled, 2, 'Setter may not be called after unsubscribe');
38+
});

subscribe.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Overrides Meteor.subscribe to do the following things:
3+
* - Automatically stops the subscription when the component is destroyed
4+
* - Makes the return value usable in {#await} blocks
5+
*/
6+
7+
import { current_component } from 'svelte/internal';
8+
9+
_subscribe = Meteor.subscribe;
10+
Meteor.subscribe = function subscribe(name) {
11+
const params = Array.from(arguments);
12+
let callbacks = Object.create(null);
13+
if (params.length > 1) {
14+
// Preserve existing callbacks.
15+
const last = params[params.length - 1];
16+
if (last) {
17+
// Last arg may be specified as a function, or as an object
18+
if (typeof last === 'function') {
19+
callbacks.onReady = params.pop();
20+
} else if ([last.onReady, last.onError, last.onStop].some(f => typeof f === "function")) {
21+
callbacks = params.pop();
22+
}
23+
}
24+
}
25+
params.push(callbacks);
26+
27+
let subscription;
28+
29+
// Collect callbacks to call when subscription is ready or has errored.
30+
let readyCallbacks = [];
31+
let errorCallbacks = [];
32+
if (callbacks.onReady) {
33+
readyCallbacks.push(callbacks.onReady);
34+
}
35+
if (callbacks.onError) {
36+
errorCallbacks.push(callbacks.onError);
37+
}
38+
callbacks.onReady = () => {
39+
readyCallbacks.forEach(fn => fn(subscription));
40+
readyCallbacks.length = 0;
41+
};
42+
callbacks.onError = (err) => {
43+
errorCallbacks.forEach(fn => fn(err));
44+
errorCallbacks.length = 0;
45+
};
46+
47+
subscription = _subscribe.apply(this, params);
48+
if (current_component) {
49+
current_component.$$.on_destroy.push(subscription.stop.bind(subscription));
50+
}
51+
subscription.then = (fn, err) => {
52+
if (subscription.ready()) {
53+
fn();
54+
} else {
55+
readyCallbacks.push(fn);
56+
err && errorCallbacks.push(err);
57+
}
58+
};
59+
return subscription;
60+
};

use-tracker.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* This function wraps a reactive Meteor computation as a Svelte store.
3+
*/
4+
5+
export default function useTracker(reactiveFn) {
6+
return {
7+
subscribe(set) {
8+
const computation = Tracker.autorun(() => set(reactiveFn()));
9+
return computation.stop.bind(computation);
10+
},
11+
};
12+
};

0 commit comments

Comments
 (0)