Nothing Special   »   [go: up one dir, main page]

Skip to content
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

[WIP] Preferences #29

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Conversation

chriseidhof
Copy link
Collaborator

I'm not sure if this is a good idea. But this is one way you could implement preferences. I would love to have this supported, but it also complicates everything (it's easy to add a feature like this, but hard to remove).

I'm also not sure if this is the best way to implement this, the visitor could also return an optional and then readPreference could do nil-coalescing.

@robb
Copy link
Owner
robb commented Jun 28, 2021

Yeah, I think this is a worth-wile problem to solve.

In my website, I used a dependency filter that adds a custom tag.

Basically, you have markup that read like this:

<html>
    <head></head>
    <body>
        <custom-script-dependency src="/foo.js" async="true">
    </body>
</html>

that get's rewritten using two Visitors to

<html>
    <head>
        <script src="/foo.js" async></script>
    </head>
    <body></body>
</html>

I like the simplicity of keeping everything in Node-space. Missing the preprocessing step is also relatively simple since there's always an inspectable output and no "invisible nodes" that don't effect the outcome.

I wonder if we could mix these approaches where preferences on a Node get merged into the attributes property but are all prefixed with swim-, so maybe something like

let body = article {
    "Lorem ipsum dolor sit amet."
}
.preference(ScriptDependencyKey.self, ScriptDependency(src: "/foo.js", async: true))

would, assuming it's not filtered out by default in a pre-processing-step, turn into

<article swim-script-dependency="{src: &quot:foo.js&quot:, async: &quot:true&quot:}">
    Lorem ipsum dolor sit amet.
</article>

Not sure if purging these attributes by rewriting the notes is better than suppressing them in rendering.

In #28 I played around with relaxing the requirements for attribute values to AnyHashable and it seems to be pretty smooth sailing, only requiring updates to visitor implementations.

That said, I'm also open to saying we need a Sail library that adds a View-like protocol (that Node could also conform to) that introduces Preferences and an Environment and that this is beyond the scope of Swim.

@chriseidhof
Copy link
Collaborator Author

Yes, if we want to keep things separate we'd probably also need a separate result builder. Also not sure how it would work with things like the built-in HTML tags. What would be the type of the child nodes?

@robb
Copy link
Owner
robb commented Jun 28, 2021

I was thinking something like this:

protocol Component {
    @ComponentBuilder
    var body: some Component

    func render() -> Node
}

extension Component {
    func render() -> Node {
        body.render()
    }
}

extension Node: Component {
    @ComponentBuilder
    var body: Node { self }

    func render() -> Node {
        self
    }
}

struct TabBar: Component {
    var tabs: [Tab]

    @ComponentBuilder
    var body: some Component {
        ul {
            tabs.map { tab in
                li {
                    tab
                }
            }
        }
        // Hypothetical Component-modifier wrapper around adding a dependency
        // using `DependencyPreferenceKey` that reduces by adding to a set.
        .dependency(src: "/tabs.js", async: false)
    }
}

struct Page<Content>: Component where Content: Component {
    var content: Content

    @ComponentBuilder
    var body: some Component {
        html {
            head {
                // Hypothetical wrapper around reading all dependencies using
                // `DependencyPreferenceKey`.
                content.dependencies.map { dependency in
                    script(src: dependency.src, async: dependency.async)
                }
            }
            body {
                content
            }
        }
    }
}

Might be missing something obvious that makes this unworkable tho?

@chriseidhof
Copy link
Collaborator Author

I think this could work! I'll also play around with this approach, hopefully I'll find some time tomorrow.

@robb
Copy link
Owner
robb commented Jun 28, 2021

I guess I handwaived the preferences, but I think they could work something like this?

struct PreferenceWriter<Content>: Component where Content: Component {
    var content: Content

    var preferences: [String: AnyHashable]

    @ComponentBuilder
    var body: some Component {
        content
    }

    func render() -> Node {
        // Produces something like <sail-preference for="bar" /> if rendered by
        // mistake but should usually be stripped.
        //
        // Putting prefixed attributes on `content.rendered()` would be nicer
        // but it could be a `Node.text` or `Node.trim` where that wouldn't
        // work.
        PreferenceTag(preferences: preferences)

        content.rendered()
    }
}

extension Component {
    var preferences: [String: AnyHashable] {
        let rendered = render()

        // Finds every `PreferenceTag`, merges preferences
        let visitor = PreferenceVisitor(content: rendered)

        return visitor.preferences
    }
}

This is a bit inefficient since we'd be visiting every Node but it avoids introducing a parallel parent-child relationship between Components, hmm.

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