Skip to content

Currying Links ? #669

Description

@gossi

So, let's say this setup:

  • We have a component that represents a page/screen
  • The component is injected into two different hosts (let's say app and engine).
  • The component will link to a particular page
  • The component knows about that target route and what kind of parameters to pass in
  • Yet the component doesn't know under which name the route is available in the current host

As routes are the connectivity layer - they have knowledge about route names but at route level the information about what parameters to be put in are not directly present.

FWIW is to curry links. Ie. the route passes in the link/name of the route and the component curries it with the parameters. I assume something like this code:

{{! route.hbs }}
<MyPage @detailLink={{link 'go-to-details'}} />
{{! component }}

<Button @link={{link @detailLink this.model.id}}>...</Button>

Now, this example looks like as if it is enough to pass in the route name, but also on route level you shall be able to pass generic parameters in as well, so within the component it is only to "finalize" the link.

thoughts?

Activity

  1. buschtoens commented on Jan 19, 2022

    @buschtoens
    Owner

    I think overall this is a great and useful suggestion. I can definitely see myself wanting to use such a feature in some situations. Thank you.

    But I fear the proposed mechanics won't work reliably:

    Issues with Models / Dynamic Segments

    Partial Application Base Final Wrapper
    {{! route.hbs }}
    <MyPage @detailLink={{link "go-to-details"}} />
    {{! components/my-page.hbs }}
    <Button @link={{link @detailLink this.model.id}}>...</Button>

    {{link "go-to-details"}} itself is missing a model, which is then provided one layer down by the wrapping {{link @detailLink this.model.id}} invocation.

    While the latter is a valid Link then, the former isn't, but there's no (convenient) way to tell them apart. If you were to access @detailLink.url, things will likely blow up, because RouterService#urlFor(...) is invoked with too few arguments.

    Additionally, there may be routes accepting more than one model, e.g. /user/:user_name/address/:address_slug. How would this support statically partially applying one model in the base invocation and dynamically applying the other model in the final wrapper invocation?

    Naïve Solution

    One option is to naïvely append models from left to right:

    {{#let (link "user.address" "buschtoens") as |baseLink|}}
      {{#let (link baseLink "home") as |finalLink|}}
        <a href={{finalLink.url}}>Home</a>
      {{/let}}
    
      {{#let (link baseLink "work") as |finalLink|}}
        <a href={{finalLink.url}}>Work</a>
      {{/let}}
    {{/let}}

    In this case :user_name is statically applied as "buschtoens" in the base invocation. The wrapping invocation then can dynamically apply :address_slug: "home" or "work" in this case.

    But what if you need :user_name to be dynamic instead? Since models are applied from left to right, that won't work. For routes taking even more models, it gets even worse.

    Issues with Query Params

    For query params it's not as bad, because they are named instead of positional. However, that introduces a second problem: Wrapping invocations may unintentionally override query params that were set in the base invocation. This can lead to bugs. But in a different scenario, this behavior may be desirable.

    Intermittent Solution

    I think we need to bikeshed this a bit more and consider various use cases.

    For the time being, you could easily achieve this with a custom (inline) helper. It even gives you maximum control over how arguments are applied.

    import { helper } from '@ember/helper';
    import { inject as service, Registry as Services } from '@ember/service';
    import Component  from '@glimmer/component';
    
    export default class MyComponent extends Component {
      @service('link-manager')
      private declare linkManager: Services['link-manager'];
    
      readonly buildDetailsLink = helper((id: string) => this.createUILink({
        route: 'go-to-details',
        models: [id]
      });
    }
    <MyPage @detailsLink={{this.buildDetailsLink}} />
    <a href={{get (@detailsLink this.model.id) "url"}}>...</a>

    Using ember-functions-as-helper-polyfill the component class would become a bit more concise:

    import { inject as service, Registry as Services } from '@ember/service';
    import Component  from '@glimmer/component';
    
    export default class MyComponent extends Component {
      @service('link-manager')
      private declare linkManager: Services['link-manager'];
    
      buildDetailsLink(id: string) {
        return this.createUILink({
          route: 'go-to-details',
          models: [id]
        });
      }
    }

    Potential ember-link Support

    Considering that Ember is evolving towards easier & convenient interoperation of templates & JS, this pattern may turn out to be preferable anyway.

    We can trim down this code even further with a utility function. We might even add a static method on Link / UILink, like this:

    import Component  from '@glimmer/component';
    import { UILink } from 'ember-link';
    
    export default class MyComponent extends Component {
      readonly buildDetailsLink = (id: string) => UILink.for(this, {
        route: 'go-to-details',
        models: [id]
      });
    }
    class Link {
      // ...
    
      constructor(linkManager: LinkManagerService, params: LinkParams) {
        // ...
      }
    
      static for(
        ownedObject: object,
        linkParams: LinkParams
      ): this {
        const owner = getOwner(ownedObject);
        const linkManager = owner.lookup('service:link-manager');
        return new this(linkManager, linkParams);
      }
    }
  2. gossi commented on Jan 22, 2022

    @gossi
    CollaboratorAuthor

    Aaaah, thanks for highlighting that currying cannot always work. It was just my initial case in which it worked.

    However, really like the idea for the factory method (esp. with ember-functions-as-helper-polyfill).

    Let's do this 💪

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions