Skip to content

Commit

Permalink
Add querySubscription
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanoverna committed Jul 24, 2024
1 parent 5b75af2 commit 90f4ec1
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 72 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ Components:
- [`<StructuredText />`](src/lib/components/StructuredText)
- [`<Head />`](src/lib/components/Head)

Stores:

- [`querySubscription`](src/lib/stores/querySubscription)

## Installation

```
Expand Down
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
},
"type": "module",
"dependencies": {
"datocms-listen": "^0.1.15",
"datocms-structured-text-utils": "^4.0.1",
"svelte-intersection-observer": "^1.0.0"
},
Expand Down
144 changes: 72 additions & 72 deletions src/lib/components/NakedImage/__tests__/NakedImage.svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,92 +3,92 @@ import { fireEvent, render, screen } from '@testing-library/svelte';
import { describe, expect, it, vi } from 'vitest';

import {
completeData,
minimalData,
minimalDataWithRelativeUrl,
completeData,
minimalData,
minimalDataWithRelativeUrl
} from '../../Image/__tests__/__fixtures__/image';

import { NakedImage } from '../../..';

describe('NakedImage', () => {
describe('passing className and/or style', () => {
it('renders correctly', () => {
const onLoad = vi.fn();
describe('passing className and/or style', () => {
it('renders correctly', () => {
const onLoad = vi.fn();

const { container, component } = render(NakedImage, {
props: {
data: minimalData,
imgClass: 'img-class-name',
imgStyle: 'background: red; overflow: visible; padding: 0px 10px;',
pictureClass: 'picture-class-name',
pictureStyle: 'background: green;',
},
});
const { container, component } = render(NakedImage, {
props: {
data: minimalData,
imgClass: 'img-class-name',
imgStyle: 'background: red; overflow: visible; padding: 0px 10px;',
pictureClass: 'picture-class-name',
pictureStyle: 'background: green;'
}
});

component.$on('load', onLoad);
component.$on('load', onLoad);

const picture = screen.getByTestId('picture');
const img = screen.getByTestId('img');
const picture = screen.getByTestId('picture');
const img = screen.getByTestId('img');

fireEvent.load(img);
fireEvent.load(img);

expect(picture).toBeInTheDocument();
expect(onLoad).toHaveBeenCalled();
expect(container).toMatchSnapshot();
});
});
expect(picture).toBeInTheDocument();
expect(onLoad).toHaveBeenCalled();
expect(container).toMatchSnapshot();
});
});

describe('full data', () => {
it('renders correctly', () => {
const { container } = render(NakedImage, {
props: { data: completeData },
});
expect(container).toMatchSnapshot();
});
});
describe('full data', () => {
it('renders correctly', () => {
const { container } = render(NakedImage, {
props: { data: completeData }
});
expect(container).toMatchSnapshot();
});
});

describe('minimal data', () => {
it('renders correctly', () => {
const { container } = render(NakedImage, {
props: { data: minimalData },
});
expect(container).toMatchSnapshot();
});
});
describe('minimal data', () => {
it('renders correctly', () => {
const { container } = render(NakedImage, {
props: { data: minimalData }
});
expect(container).toMatchSnapshot();
});
});

describe('minimalDataWithRelativeUrl', () => {
it('renders correctly', () => {
const { container } = render(NakedImage, {
props: { data: minimalDataWithRelativeUrl },
});
expect(container).toMatchSnapshot();
});
});
describe('minimalDataWithRelativeUrl', () => {
it('renders correctly', () => {
const { container } = render(NakedImage, {
props: { data: minimalDataWithRelativeUrl }
});
expect(container).toMatchSnapshot();
});
});

describe('priority=true', () => {
it('renders correctly', () => {
const { container } = render(NakedImage, {
props: { data: minimalData, priority: true },
});
expect(container).toMatchSnapshot();
});
});
describe('priority=true', () => {
it('renders correctly', () => {
const { container } = render(NakedImage, {
props: { data: minimalData, priority: true }
});
expect(container).toMatchSnapshot();
});
});

describe('usePlaceholder=false', () => {
it('renders correctly', () => {
const { container } = render(NakedImage, {
props: { data: minimalData, usePlaceholder: false },
});
expect(container).toMatchSnapshot();
});
});
describe('usePlaceholder=false', () => {
it('renders correctly', () => {
const { container } = render(NakedImage, {
props: { data: minimalData, usePlaceholder: false }
});
expect(container).toMatchSnapshot();
});
});

describe('explicit sizes', () => {
it('renders correctly', () => {
const { container } = render(NakedImage, {
props: { data: minimalData, sizes: '(max-width: 600px) 200px, 50vw' },
});
expect(container).toMatchSnapshot();
});
});
describe('explicit sizes', () => {
it('renders correctly', () => {
const { container } = render(NakedImage, {
props: { data: minimalData, sizes: '(max-width: 600px) 200px, 50vw' }
});
expect(container).toMatchSnapshot();
});
});
});
2 changes: 2 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export { default as Image } from './components/Image/Image.svelte';
export { default as StructuredText } from './components/StructuredText/StructuredText.svelte';
export { default as VideoPlayer } from './components/VideoPlayer/VideoPlayer.svelte';

export * from './stores/querySubscription';

export type PredicateComponentTuple = [
(n: Node) => boolean,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
107 changes: 107 additions & 0 deletions src/lib/stores/querySubscription/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Live real-time updates

`querySubscription` returns a Svelte store that you can use to implement client-side updates of the page as soon as the content changes. It uses DatoCMS's [Real-time Updates API](https://www.datocms.com/docs/real-time-updates-api/api-reference) to receive the updated query results in real-time, and is able to reconnect in case of network failures.

Live updates are great both to get instant previews of your content while editing it inside DatoCMS, or to offer real-time updates of content to your visitors (ie. news site).

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Initialization options](#initialization-options)
- [Connection status](#connection-status)
- [Error object](#error-object)
- [Example](#example)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

```
## Reference
Import `querySubscription` from `datocms-svelte` and use it inside your components like this:
```js
import { querySubscription } from '@datocms/svelte';
const subscription = querySubscription(options: Options);
```

## Initialization options

| prop | type | required | description | default |
| ------------------ | ------------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------------------------------ | ------------------------------------ |
| enabled | boolean | :x: | Whether the subscription has to be performed or not | true |
| query | string \| [`TypedDocumentNode`](https://github.com/dotansimha/graphql-typed-document-node) | :white_check_mark: | The GraphQL query to subscribe | |
| token | string | :white_check_mark: | DatoCMS API token to use | |
| variables | Object | :x: | GraphQL variables for the query | |
| preview | boolean | :x: | If true, the Content Delivery API with draft content will be used | false |
| environment | string | :x: | The name of the DatoCMS environment where to perform the query | defaults to primary environment |
| initialData | Object | :x: | The initial data to use on the first render | |
| reconnectionPeriod | number | :x: | In case of network errors, the period (in ms) to wait to reconnect | 1000 |
| fetcher | a [fetch-like function](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) | :x: | The fetch function to use to perform the registration query | window.fetch |
| eventSourceClass | an [EventSource-like](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) class | :x: | The EventSource class to use to open up the SSE connection | window.EventSource |
| baseUrl | string | :x: | The base URL to use to perform the query | `https://graphql-listen.datocms.com` |

## Connection status

The `status` property represents the state of the server-sent events connection. It can be one of the following:

- `connecting`: the subscription channel is trying to connect
- `connected`: the channel is open, we're receiving live updates
- `closed`: the channel has been permanently closed due to a fatal error (ie. an invalid query)

## Error object

| prop | type | description |
| -------- | ------ | ------------------------------------------------------- |
| code | string | The code of the error (ie. `INVALID_QUERY`) |
| message | string | An human friendly message explaining the error |
| response | Object | The raw response returned by the endpoint, if available |

## Example

```svelte
<script>
import { querySubscription } from 'react-datocms';
const subscription = useQuerySubscription({
enabled: true,
query: `
query AppQuery($first: IntType) {
allBlogPosts {
slug
title
}
}`,
variables: { first: 10 },
token: 'YOUR_API_TOKEN',
});
$: ({ data, error, status } = $subscription)
const statusMessage = {
connecting: 'Connecting to DatoCMS...',
connected: 'Connected to DatoCMS, receiving live updates!',
closed: 'Connection closed',
};
</script>
<p>Connection status: {statusMessage[status]}</p>
{#if error}
<h1>Error: {error.code}</h1>
<p>{error.message}</p>
{#if error.response}
<pre>{JSON.stringify(error.response, null, 2)}</pre>
{/if}
{/if}
{#if data}
<ul>
{#each data.allBlogPosts as blogPost (blogPost.slug)}
<li>{blogPost.title}</li>
</ul>
{/if}
```
Loading

0 comments on commit 90f4ec1

Please sign in to comment.