Skip to content

List things related to a resource with a custom display #119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
375cd10
feat: pos-list creates children for specified rel
jg10-mastodon-social May 12, 2025
49e211b
feat: pos-list renders the provided template
jg10-mastodon-social May 24, 2025
322217b
feat: pos-list provides resources to descendants
jg10-mastodon-social May 25, 2025
9d93a11
feat(core): thing.relations takes optional predicate
jg10-mastodon-social May 26, 2025
cb1b056
refactor(pos-list): pass predicate to resource.relations
jg10-mastodon-social May 26, 2025
ac79622
test: pos-list integration
jg10-mastodon-social Jun 7, 2025
7686e6b
doc: add story for pos-list composition
jg10-mastodon-social Jun 7, 2025
5b12544
fix(pos-list): unused import
jg10-mastodon-social Jun 7, 2025
19ca31a
doc: generate pos-list docs
jg10-mastodon-social Jun 7, 2025
3d7d145
doc(pos-list): add to changelog
jg10-mastodon-social Jun 7, 2025
c311e05
doc: update Thing.relations changelog
jg10-mastodon-social Jun 7, 2025
27f794e
doc(pos-list): improve unit test readability
jg10-mastodon-social Jun 16, 2025
1d302ab
test(pos-list): remove incorrect shadow root
jg10-mastodon-social Jun 16, 2025
2394290
refactor: pos-list uses pos-resource instead of providing resources i…
jg10-mastodon-social Jun 16, 2025
d105825
fix(pos-list): use const
jg10-mastodon-social Jun 16, 2025
9ef6cb1
test(pos-list): control fetching with fetch attribute
jg10-mastodon-social Jun 16, 2025
cf9d2a7
feat(pos-list): support fetch attribute
jg10-mastodon-social Jun 16, 2025
108b8e1
refactor(pos-list): split integration tests to avoid test pollution
jg10-mastodon-social Jun 22, 2025
d1a0f69
doc: pos-list basic story
jg10-mastodon-social Jun 22, 2025
90f6466
fix(doc): move Thing.relations to Unreleased
jg10-mastodon-social Jun 22, 2025
96a36ef
fix(pos-list): code cleanup
jg10-mastodon-social Jun 28, 2025
f44002a
fix(pos-list): composition story now uses ul
jg10-mastodon-social Jun 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Changed

- `Thing.relations()`: Now takes optional predicate to filter by

## 0.16.1

### Fixed
Expand Down
27 changes: 27 additions & 0 deletions core/src/thing/Thing.relations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,31 @@ describe("Thing", function () {
expect(result).toEqual([]);
});
});

it("only follows the given predicate if provided", () => {
const store = graph();
const uri = "https://jane.doe.example/container/file.ttl#fragment";
store.add(
sym(uri),
sym("http://vocab.test/first"),
sym("https://pod.example/first"),
);
store.add(
sym(uri),
sym("http://vocab.test/second"),
sym("https://pod.example/second"),
);
const it = new Thing(
"https://jane.doe.example/container/file.ttl#fragment",
store,
);
const result = it.relations("http://vocab.test/first");
expect(result).toEqual([
{
predicate: "http://vocab.test/first",
label: "first",
uris: ["https://pod.example/first"],
},
]);
});
});
7 changes: 5 additions & 2 deletions core/src/thing/Thing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ export class Thing {
}));
}

relations(): Relation[] {
const statements = this.store.statementsMatching(sym(this.uri));
relations(predicate?: string): Relation[] {
const statements = this.store.statementsMatching(
sym(this.uri),
predicate ? sym(predicate) : null,
);

const values = statements
.filter((it) => isNamedNode(it.object) && !isRdfType(it.predicate))
Expand Down
40 changes: 40 additions & 0 deletions docs/elements/components/pos-list/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

<!-- Auto Generated Below -->


## Properties

| Property | Attribute | Description | Type | Default |
| -------- | --------- | ----------------------------------------------------------------- | --------- | ----------- |
| `fetch` | `fetch` | Whether listed resources should be fetched before being displayed | `boolean` | `false` |
| `rel` | `rel` | URI of the predicate to follow | `string` | `undefined` |


## Events

| Event | Description | Type |
| ----------------- | ----------- | ------------------ |
| `pod-os:resource` | | `CustomEvent<any>` |


## Dependencies

### Depends on

- [pos-resource](../pos-resource)

### Graph
```mermaid
graph TD;
pos-list --> pos-resource
pos-resource --> ion-progress-bar
pos-resource --> ion-card
pos-resource --> ion-card-header
pos-resource --> ion-card-content
ion-card --> ion-ripple-effect
style pos-list fill:#f9f,stroke:#333,stroke-width:4px
```

----------------------------------------------

*Built with [StencilJS](https://stenciljs.com/)*
2 changes: 2 additions & 0 deletions docs/elements/components/pos-resource/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Type: `Promise<void>`

- [pos-app-browser](../../apps/pos-app-browser)
- [pos-container-contents](../pos-container-contents)
- [pos-list](../pos-list)
- [pos-login](../pos-login)
- [pos-make-findable](../pos-make-findable)
- [pos-rich-link](../pos-rich-link)
Expand All @@ -61,6 +62,7 @@ graph TD;
ion-card --> ion-ripple-effect
pos-app-browser --> pos-resource
pos-container-contents --> pos-resource
pos-list --> pos-resource
pos-login --> pos-resource
pos-make-findable --> pos-resource
pos-rich-link --> pos-resource
Expand Down
7 changes: 7 additions & 0 deletions elements/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added

- [pos-list](../docs/elements/components/pos-list):
- Basic implementation of element to list things related to a resource with a custom display

## 0.25.2

### Fixed
Expand Down
45 changes: 45 additions & 0 deletions elements/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ export namespace Components {
}
interface PosLabel {
}
interface PosList {
/**
* Whether listed resources should be fetched before being displayed
*/
"fetch": boolean;
/**
* URI of the predicate to follow
*/
"rel": string;
}
interface PosLiterals {
}
interface PosLogin {
Expand Down Expand Up @@ -183,6 +193,10 @@ export interface PosLabelCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLPosLabelElement;
}
export interface PosListCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLPosListElement;
}
export interface PosLiteralsCustomEvent<T> extends CustomEvent<T> {
detail: T;
target: HTMLPosLiteralsElement;
Expand Down Expand Up @@ -508,6 +522,23 @@ declare global {
prototype: HTMLPosLabelElement;
new (): HTMLPosLabelElement;
};
interface HTMLPosListElementEventMap {
"pod-os:resource": any;
}
interface HTMLPosListElement extends Components.PosList, HTMLStencilElement {
addEventListener<K extends keyof HTMLPosListElementEventMap>(type: K, listener: (this: HTMLPosListElement, ev: PosListCustomEvent<HTMLPosListElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
removeEventListener<K extends keyof HTMLPosListElementEventMap>(type: K, listener: (this: HTMLPosListElement, ev: PosListCustomEvent<HTMLPosListElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}
var HTMLPosListElement: {
prototype: HTMLPosListElement;
new (): HTMLPosListElement;
};
interface HTMLPosLiteralsElementEventMap {
"pod-os:resource": any;
}
Expand Down Expand Up @@ -845,6 +876,7 @@ declare global {
"pos-image": HTMLPosImageElement;
"pos-internal-router": HTMLPosInternalRouterElement;
"pos-label": HTMLPosLabelElement;
"pos-list": HTMLPosListElement;
"pos-literals": HTMLPosLiteralsElement;
"pos-login": HTMLPosLoginElement;
"pos-login-form": HTMLPosLoginFormElement;
Expand Down Expand Up @@ -966,6 +998,17 @@ declare namespace LocalJSX {
interface PosLabel {
"onPod-os:resource"?: (event: PosLabelCustomEvent<any>) => void;
}
interface PosList {
/**
* Whether listed resources should be fetched before being displayed
*/
"fetch"?: boolean;
"onPod-os:resource"?: (event: PosListCustomEvent<any>) => void;
/**
* URI of the predicate to follow
*/
"rel"?: string;
}
interface PosLiterals {
"onPod-os:resource"?: (event: PosLiteralsCustomEvent<any>) => void;
}
Expand Down Expand Up @@ -1091,6 +1134,7 @@ declare namespace LocalJSX {
"pos-image": PosImage;
"pos-internal-router": PosInternalRouter;
"pos-label": PosLabel;
"pos-list": PosList;
"pos-literals": PosLiterals;
"pos-login": PosLogin;
"pos-login-form": PosLoginForm;
Expand Down Expand Up @@ -1146,6 +1190,7 @@ declare module "@stencil/core" {
"pos-image": LocalJSX.PosImage & JSXBase.HTMLAttributes<HTMLPosImageElement>;
"pos-internal-router": LocalJSX.PosInternalRouter & JSXBase.HTMLAttributes<HTMLPosInternalRouterElement>;
"pos-label": LocalJSX.PosLabel & JSXBase.HTMLAttributes<HTMLPosLabelElement>;
"pos-list": LocalJSX.PosList & JSXBase.HTMLAttributes<HTMLPosListElement>;
"pos-literals": LocalJSX.PosLiterals & JSXBase.HTMLAttributes<HTMLPosLiteralsElement>;
"pos-login": LocalJSX.PosLogin & JSXBase.HTMLAttributes<HTMLPosLoginElement>;
"pos-login-form": LocalJSX.PosLoginForm & JSXBase.HTMLAttributes<HTMLPosLoginFormElement>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { newSpecPage } from '@stencil/core/testing';
import { mockPodOS } from '../../test/mockPodOS';
import { PosApp } from '../pos-app/pos-app';
import { PosLabel } from '../pos-label/pos-label';
import { PosList } from './pos-list';
import { PosResource } from '../pos-resource/pos-resource';
import { when } from 'jest-when';

describe('pos-list', () => {
it('fetches resources if fetch attribute is present', async () => {
const os = mockPodOS();
when(os.store.get)
.calledWith('https://resource.test')
.mockReturnValue({
relations: () => [
{
predicate: 'http://schema.org/video',
uris: ['https://video.test/video-1', 'https://video.test/video-2'],
},
],
});
await newSpecPage({
components: [PosApp, PosLabel, PosList, PosResource],
supportsShadowDom: false,
html: `
<pos-app>
<pos-resource uri="https://resource.test" lazy="">
<pos-list rel="http://schema.org/video" fetch>
<template>
<pos-label />
</template>
</pos-list>
</pos-resource>
</pos-app>`,
});

expect(os.fetch.mock.calls).toHaveLength(2);
expect(os.fetch.mock.calls).toEqual([['https://video.test/video-1'], ['https://video.test/video-2']]);
});
});
68 changes: 68 additions & 0 deletions elements/src/components/pos-list/pos-list.integration.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { newSpecPage } from '@stencil/core/testing';
import { mockPodOS } from '../../test/mockPodOS';
import { PosApp } from '../pos-app/pos-app';
import { PosLabel } from '../pos-label/pos-label';
import { PosList } from './pos-list';
import { PosResource } from '../pos-resource/pos-resource';
import { when } from 'jest-when';

describe('pos-list', () => {
it('children render label for loaded resources (without fetching)', async () => {
const os = mockPodOS();
when(os.store.get)
.calledWith('https://resource.test')
.mockReturnValue({
relations: () => [
{
predicate: 'http://schema.org/video',
uris: ['https://video.test/video-1', 'https://video.test/video-2'],
},
],
});
when(os.store.get)
.calledWith('https://video.test/video-1')
.mockReturnValue({ uri: 'https://video.test/video-1', label: () => 'Video 1' });
when(os.store.get)
.calledWith('https://video.test/video-2')
.mockReturnValue({ uri: 'https://video.test/video-2', label: () => 'Video 2' });
const page = await newSpecPage({
components: [PosApp, PosLabel, PosList, PosResource],
supportsShadowDom: false,
html: `
<pos-app>
<pos-resource uri="https://resource.test" lazy="">
<pos-list rel="http://schema.org/video">
<template>
<pos-label />
</template>
</pos-list>
</pos-resource>
</pos-app>`,
});
expect(os.fetch.mock.calls).toHaveLength(0);

const resources = page.root ? page.root.querySelectorAll('pos-list pos-resource') : [];
expect(resources).toHaveLength(2);

const label1 = resources[0] as unknown as PosResource;
expect(label1).toEqualHtml(`
<pos-resource about="https://video.test/video-1">
<pos-label>
Video 1
</pos-label>
</pos-resource>
`);
//Tested separately because pos-resource does not reflect the uri property as an attribute
expect(label1?.uri).toEqual('https://video.test/video-1');

const label2 = resources[1] as unknown as PosResource;
expect(label2).toEqualHtml(`
<pos-resource about="https://video.test/video-2">
<pos-label>
Video 2
</pos-label>
</pos-resource>
`);
expect(label2?.uri).toEqual('https://video.test/video-2');
});
});
Loading