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

Currying Links ? #669

Open
gossi opened this issue Nov 19, 2021 · 2 comments
Open

Currying Links ? #669

gossi opened this issue Nov 19, 2021 · 2 comments

Comments

@gossi
Copy link
Collaborator

gossi commented Nov 19, 2021

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?

@buschtoens
Copy link
Owner

buschtoens commented Jan 19, 2022

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);
  }
}

@gossi
Copy link
Collaborator Author

gossi commented Jan 22, 2022

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
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants