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

Encode Behavior in Link Primitive + Building Blocks #578

Closed
gossi opened this issue Apr 12, 2021 · 1 comment · Fixed by #770
Closed

Encode Behavior in Link Primitive + Building Blocks #578

gossi opened this issue Apr 12, 2021 · 1 comment · Fixed by #770

Comments

@gossi
Copy link
Collaborator

gossi commented Apr 12, 2021

The Problem

ember-link provides a primitive to pass around. It's a bit of fuzzy what it is exactly. It can be seen as just a DTO and on the other hand provides operations how to deal with the link itself (such as transitionTo() and replaceWith() methods).

Using them together is fine, when the owner of the primitive is also in control of how a link get's attached to the UI. This no longer works when the two split (ie. when the primitive is passed around).

A Solution

In order to make it work, this needs handling on two sides of the problem:

  1. The owner needs to express his wished behavior on the primitive itself
  2. On the attaching sides, it needs one interface, the implementor will use - and behind that interface the expressed behavior from (1) is executed.

A sample for (1) - making up some possibly imaginery code (as I don't have the full and correct API in my head):

this.linkManager.createUILink({ route: 'target-route', behavior: { replaceWith: true });
this.linkManager.createUILink({ url: 'https://example.com', behavior: { openInNewWindow: true });

Onto the second part of the solution (2). Let's say, this we received our link in a component as @link argument:

<a href={{@link.url}} {{on "click" @link.open}} {{link-attributes @link}}>Click me</a>

There are two additions here:

  • @link.open - which would be used to either call transitionTo() or replaceWith() on the link
  • {{link-attributes}} - which would put all attributes onto the element as needed (e.g. target="_blank" to open in a new window)

This is partially the idea from #333 - yet a little bit more exhaustive. First, it is good to have these multiple pieces as building blocks (see below on thiird party implementors). There maybe can also be just one modifier that handles all of the above @link related markup in one place.
As implementor, using that interface would give me the chill, that I attached the link properly to my markup.

For third party implementors, the exhaustive building blocks might be more suitable. See here: https://github.com/gossi/ember-command/blob/9101687488c9c20e4c7e9510632f44c38cd88a1f/addon/components/commander/index.hbs

@gossi
Copy link
Collaborator Author

gossi commented May 3, 2021

1st Attempt

Here is the idea for my first attempt, which is basically shoving symmetry in creating links as well as in attaching them to elements (after you created them and send them off as primitive). Giving link creators the ability to store all intentions and tools for implementors to respect them.

I'll explain at some highlights of described API changes below - this is a way to give clear opinion on how to use ember-link, while removing alternative options that are only here for unnecessary complexity bloat (e.g. positional arguments for the (link) helper).

Creating Links

/** target can be either of these */
type target = RouteName | RouteURL | URL;

/** aka. LinkParams */
interface LinkOptions {
  models?: RouteModel;
  query?: QueryParams;
  behavior: {
    /** @defaultValue transition */
    history: 'transition' | 'replace';
    preventDefault: boolean;
  },
  /** https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attributes */
  attributes: Record<string, string>;
}
  • linkManager.createLink(target: Target, options: LinkOptions);
  • (link target options)

Positional args: required (target)
Named args: optional

Same way to create links in declarative and imperative form 😎
From public API it shouldn't matter what target you choose. The mechanics for the links shall be the same.

Link is the only primitive left (no subclasses allowed). createUILink() will be marked deprecated.

Opening Behavior

Whoever is responsible for attaching the link to the element is in charge of the link stacks onto the history (using transitionTo() or replaceWith()) which might be orthogonal to whatever the intention was, when the link was created. That's why there is now a behavior bucket, that encodifies that intention. As a matter of fact, link shall have an additional open() method onto them, which respects this behavior. Use as follows:

<a href={{link.url}} {{on "click" link.open}}>

So an implementer is safe to respect the intention for opening.
So, link.open() will use transition or replace respectively.

Attaching

And the attachers:

  • {{attach-link-attributes link}} - low level building block to attach attributes if they do not already exist
  • {{attach-link link}} = {{attach-link-attributes}} + event handler

Use the latter and you should be good. Use the former, if you take care of invoking business already and want to treat links, too:

<a {{attach-link-attributes link}} {{on "click" link.open}}>Low Level</a>

<a {{attach-link link}}>High Level</a>

The Problem

I'm really happy about the public API expressed above. The main problem is in how it is processed under the hood. Mainly I see three ways of doing so:

  • Route by Name (inside the current engine, local scope)
  • Route by URL (inside the host of the engine, global scope)
  • URLs not part of the app

Routes expose a different public API as links. They have all the getters in which state they are in to be used for styling and also control the history behavior (transition vs replace). Whereas just URLs do not know this.

Solution 1

Would be to explcitely differentiate between routes know to the app and just URLs in terms of API surface: (link-url) and (link) which would return Link and RouteLink respectively.

Dealing with two links is already not ideal as the current Link and UILink shows. There shall only be one Link (the highlander rule). Also we offload this complexity as mental burden onto implementors, much like engines to with transitionTo() and transitionToExternal() - nobody shall ever care about using one or the other. Same API everywhere.

Solution 2

When creating a link, we can assign it a handler. A RouteHandler, a RouteURLHandler and a URLHandler, where each would take responsibility for handling each of these different targets. That is under the assumption, that handling a route in the host app (route url) and inside the current engine (route name) is different (anyway only one route handler is needed). The Link itself acts as proxy and forwards getters and methods to the handler, example:

get isActive() {
  return this.handler.isActive;
}

open(event?: Event) {
  this.handler.open(event);
}

That would enable for the Link to have a stable API and a handler can return falsy/void values for any unsupported prop/method.

Solution 3

Taken from making illegal states unrepresentable is to some extend yield different APIs in relation to the identified target. While this works quite nice for the shown (load) example in the blog post, dunno how in particularly this can work for the link 🤷

Summary

  • Consolidate the API and making it symmetric will be beneficial to use and to provide support for
  • This public API change can be made backwards compatible - we put in deprecation messages
  • I'm not yet sure about the internal workings, my gut feeling is going with proposed solution 2 unless some clever mind can find an implementation as dreamed of in solution 3.

@gossi gossi mentioned this issue Apr 4, 2023
9 tasks
@gossi gossi closed this as completed Jun 27, 2023
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 a pull request may close this issue.

1 participant