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 11 commits into
base: main
Choose a base branch
from

Conversation

jg10-mastodon-social
Copy link
Collaborator

@jg10-mastodon-social jg10-mastodon-social commented May 24, 2025

Partly addresses #43

This PR descoped to exclude rev, given this will involve changes very similar to rel. Covering rev in a separate PR will hopefully make both PRs easier to review.

if (templateElement == null) {
this.error = "No template element found"
} else if (templateElement.content.childElementCount != 1) {
this.error = "Template element should only have one child, e.g. li"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for this is that the component will provide resources to each child based on their index. Compared to #43 (comment) this gets rid of the intermediate pos-resource. However, it will still not be possible to have li nested directly within ul, given that pos-list lies between them.

Copy link
Collaborator Author

@jg10-mastodon-social jg10-mastodon-social May 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One option would be to allow pos-list to be used as a customized built-in element, i.e. <ul is="pos-list"></ul>. I would need to look into whether/how stenciljs supports this.

Update: stenciljs does not support customized built-in elements stenciljs/core#4089 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about assigning role="list"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue I had in mind is that the only permitted parents for li are ul and ol (or menu). So if a dashboard user cares about correct HTML then I suppose they could use <div role="listitem" style="display:list-item"> I hadn't thought of that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've currently ignored correct HTML for this PR. In storybook I've used bare lis. Let me know if it's an issue.


receiveResource = (resource: Thing) => {
this.items = []
if (this.rel) this.items = resource.relations().filter(relation => relation.predicate == this.rel).flatMap(relation => relation.uris);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead of querying all relations and then filter Thing should provide a method to get the relation in question directly. Also is it limited to relations to other resources or could the predicate refer to literals as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I'll work on that. There'll be a similar method for rev. Literals are out of scope for this particular PR but will need to be revisited as part of #105 . I'll think about options to handle forward compatibility.

});
expect(page.root).toEqualHtml(`
<pos-list rel="http://schema.org/video" />
<template><div>Test</div></template>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use template and not (named) slots?

https://stenciljs.com/docs/templating-jsx#slots

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens within a list is that templated content gets repeated and the original template should never actually be rendered. Slots could be used but to me the semantics are all wrong. We're not actually slotting anything anywhere - we're using a template to create a new set of items.

async provideResource(event: SubscribeResourceEvent) {
event.stopPropagation();
this.provideQueue.push(event);
this.provideResources();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In contrast to pos-resource, pos-list is in control of the uris of children, so we don't need to store consumers and call them when uris change. However, we do still need to deal with race conditions where provideResource is called before this.os has been set, which is why I'm using a queue. We will eventually also need to deal with updating the children when this.items changes, but that's out of scope of this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not get the need for the provide queue yet, tbh. Is there a respective test that fails if no queue is used that helps to highlight the problem?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iguess, while this.os is not present the list should not start rendering anything. If it doesnt there is no chance of children requesting a resource. After that we could just provide the resource as soon as requested, without a queue?

while (this.provideQueue.length > 0) {
let ev = this.provideQueue.pop();
let resourceUri = ev.currentTarget.getAttribute('about');
let resource = this.os.store.get(resourceUri);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a subject-connected list, I could use Thing methods directly. I've used this architecture so that the same component will be able to handle enumerating lists too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've also excluded fetching from this PR - the current list element will only display already loaded resources.

This is consistent with pos-list being lazy-first - it's a display helper that only secondarily triggers network access. A follow up PR with fetching will also need to revisit how core is dealing with repeated fetches of the same resource.

"https://jane.doe.example/container/file.ttl#fragment",
store,
);
const result = it.relations("http://vocab.test/first");
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm suggesting this solution because it results in a minimal and backward compatible change to the core API. If we were to add a completely new method, it would be useful to rationalise what return types we want to provide, given that we're departing from RDF/JS specifications.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not 100% convinced about this, but if this works for the list we can go this path for now

</template>
</pos-list>:
<pos-value predicate="http://www.w3.org/ns/solid/terms#publicId"/></pos-value>
</li>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of nested lists is a bit more complex than originally intended, but it turns out the example used in pos-relations uses schema:skills in a way that ends up like this. In the process it demonstrates the flexibility of pos-list, including being able to change subject for a singleton list (solid:publicId has a single object), which provides a basic implementation of #89. So I think I'm happy with it.

I've also put it under the composition menu because it doesn't make sense to use pos-list without pos-resource as a parent and with some kind of child.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really cool example. My first reaction was "Isn't there an easier way to get the singleton value, like pos-value" but this indeed would only provide the publicId URI and no way to get the label. So really cool to see that lists can be used that way as well.

Yet I would appreciate to have a simpler example for pos-list. Yes, you need a pos-resource and a child, but still a minimal viable example would be useful imho. Perhaps similar to what is in the unit tests. Perhaps we need a new category for this. "Templating" comes to my mind.

@jg10-mastodon-social jg10-mastodon-social marked this pull request as ready for review June 7, 2025 06:42
@jg10-mastodon-social
Copy link
Collaborator Author

I've marked as ready for review because I'm not sure what e2e test should be included (is the integration test enough?), and I think we can maybe descope and implement rev in a separate PR - this one is probably complex enough as it is?

Copy link
Contributor

@angelo-v angelo-v left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work so far! I have several comments and questions, but many of them are minor, I think this goes into a good direction.

"https://jane.doe.example/container/file.ttl#fragment",
store,
);
const result = it.relations("http://vocab.test/first");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not 100% convinced about this, but if this works for the list we can go this path for now

@@ -9,6 +9,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- `IndexedDbOfflineCache`: A PodOS cache implementation that uses IndexedDB to store data for offline usage
- [pos-list](../docs/elements/components/pos-list):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure this goes into the correct section while rebasing

.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],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could say supportsShadowDom: false, here to make the test output more consice (get rid of mock:shadow-root)

it('sets about attribute on children', async () => {
const page = await newSpecPage({
components: [PosList],
html: `<pos-list rel="http://schema.org/video"><template><div>Test</div></template></pos-list>`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some newlines would make this more readable

componentWillLoad() {
subscribePodOs(this);
subscribeResource(this);
let templateElement = this.host.querySelector('template');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be const

<this.templateNodeName
innerHTML={this.templateString}
about={it}
onPod-os:resource={ev => this.provideResource(ev)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ware we doing this instead of wrapping each child in a pos-resource?

async provideResource(event: SubscribeResourceEvent) {
event.stopPropagation();
this.provideQueue.push(event);
this.provideResources();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not get the need for the provide queue yet, tbh. Is there a respective test that fails if no queue is used that helps to highlight the problem?

@Watch('os')
private async provideResources() {
while (this.provideQueue.length > 0) {
let ev = this.provideQueue.pop();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use const whenever possible

if (resource) {
ev.detail(resource);
} else {
this.provideQueue.push(ev);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This else part is untested but also unreachable, since resource is always != null

async provideResource(event: SubscribeResourceEvent) {
event.stopPropagation();
this.provideQueue.push(event);
this.provideResources();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iguess, while this.os is not present the list should not start rendering anything. If it doesnt there is no chance of children requesting a resource. After that we could just provide the resource as soon as requested, without a queue?

@angelo-v
Copy link
Contributor

angelo-v commented Jun 9, 2025

all dependencies are updated to the latest patch version at minimum

You don't need to bother with this in the PR, I will do this after merging before releasing a new version

@angelo-v
Copy link
Contributor

angelo-v commented Jun 9, 2025

I am having an issue using this within another stencil component. If I paste an example directly in index.html it works, but the same example fails when I e.g. use it in pos-app-browser (just replacecing the regular pos-app content with the pos-list example).

In the latter case it fails, because it does not recognize the template child (childElementCount == 0). Don't know why yet.

@angelo-v
Copy link
Contributor

angelo-v commented Jun 9, 2025

It seems to be related to what is described here: https://stackoverflow.com/a/70778523

If I pass the content to <template innerHTML={"..."} instead of the template body then it works!

@angelo-v
Copy link
Contributor

angelo-v commented Jun 9, 2025

I managed to show my bookmarks using pos-list by the way, which is really cool!

<pos-app>
      <pos-resource uri="https://angelo.veltens.org/public/bookmarks">
        <pos-list rel="http://purl.org/dc/terms/references">
          <template>
            <article>
              <h2>
                <pos-label></pos-label>
              </h2>
              <pos-value predicate="http://www.w3.org/2002/01/bookmark#recalls"></pos-value>
            </article>
          </template>
        </pos-list>
      </pos-resource>
    </pos-app>

What is unfortunately still not possible with PodOS is actually linking to the value because I cannot use pos-value in an <a href for example. Not sure if we have an issue for this yet see #123

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants