mubanjs / muban

A standalone library for creating a single muban component
https://mubanjs.github.io/muban/
MIT License
9 stars 6 forks source link

RFC: Wrapper Components #100

Closed ThaNarie closed 1 year ago

ThaNarie commented 1 year ago

Partly relates to https://github.com/mubanjs/muban/projects/1#card-68019505

Wrapper Components can be described as components that render content that is passed down from above, and is owned by a parent.

Examples are:

Current Issue

Currently, any data-ref or data-component is owned and instantiated by its direct parent.

There is a workaround with ignoreGuard, but that is only there for edge cases, and isn't granular enough.

Let's look at what the HTML could look like:

<div data-component="my-component">
  <div data-component="some-wrapper">
    <span data-ref="foo">label</span>
  </div>
</div>

In this example, the data-ref="foo" wants to be managed by the data-component="my-component", but data-component="some-wrapper" is in the way (unless ignoreGuard is passed).

Proposal

Being able to "mark" a component as wrapper, so its "children" are resolved to its parent. Basically it's removing (parts of) itself from the resolving equation.

The currently resolving works as follows:

  1. A components defines a refElement or refComponent
  2. A querySelector is done to find candidates
  3. Each candidate looks up in the tree for its parent data-component element.
  4. If ignoreGuard is passed, it will just return as valid
  5. if the "owner" is found, it will return as valid
  6. Otherwise it will be invalid.

To make this wrapper work, it should do an additional step after number 5, where it continues looking upwards if it encounters a wrapper component.

indicating something as wrapper

In order to make the above work, the resolving code needs some additional information. It needs to know which data-component to skip (similar to ignoreGuard, but more granular). However, the wrapper component also need to "protect" it's own components, and those don't belong to the parent.

So besides a boundary on the parent side, it also needs a boundary on the children side. Everything those boundaries is marked internal, but everything past the children boundary belongs to the parent again.

Since we already have the data-component as attribute on the "root tag" of each component, that can serve as parent boundary. Then we only need a new attribute for the child boundary. When that attribute exists somewhere in the component, the whole component becomes a "wrapper component".

If the wrapper component exists only of a single div (e.g. because all its wrapper logic is done on JS, and not in the template), the boundary attribute can be placed on that single element.

If a wrapper component has multiple places where it can render child content, multiple boundary attributes could exist on different elements.

The name of this attribute is TBD, but let's call it data-wrapper-boundary for now.

The earlier example would then look like:

<div data-component="my-component">
  <div data-component="some-wrapper" data-wrapper-boundary>
    <span data-ref="foo">label</span>
  </div>
</div>

A less simple example, where the wrapper as a button ref, would look like this:

<div data-component="my-component">
  <div data-component="some-wrapper">
    <button data-ref="toggle">Toggle Content</button>
    <div data-ref="toggle-content" data-wrapper-boundary>
      <span data-ref="foo">label</span>
    </div>
  </div>
</div>

In the example, the data-ref="toggle" and data-ref="toggle-content" refs belong to the some-wrapper component, but the data-ref="foo" element that's inside the (or conceptually outside) the data-wrapper-boundary could now belong to my-component again.

Updated resolve logic

Once we have found the closest parent data-component, we need to know if we are inside or outside the data-wrapper-boundary. If we only look for the data-wrapper-boundary element, it can be positioned in three locations; between the data-component and the target element; further down the target element, or further up the `data-component element.

In this case we only care about looking up, but we still need to know if it's inside or above the data-component element. We can do this by also looking upwards from the the data-wrapper-boundary to find the closest data-component, and see if it's the same. If it is, we can ignore that component, and continue looking upwards.

So in short:

With this in place, it should be able to "skip" multiple wrappers as well:

<div data-component="my-component">
  <div data-component="some-wrapper" data-wrapper-boundary>
    <div data-component="some-wrapper-2" data-wrapper-boundary>
      <div data-component="some-wrapper-3" data-wrapper-boundary>
        <span data-ref="foo">label</span>
      </div>
    </div>
  </div>
</div>

In the resolve logic, all the 3 wrappers can be skipped, so the data-ref="foo" can be owned by my-component.

What can wrappers own?

If wrappers have a child boundary, should they be able to access those elements without using ignoreGuard? Since all logic is passed from parents, it should never have direct access to it. So we need to build this in the resolving logic.

This means that step number 5 above (if owner is found), needs another condition. It needs to make sure there is NO data-wrapper-boundary in between. So we can do the same logic as before, and return null if this is the case (instead of ignoring it).

So the complete resolve logic should be as follows:

  1. A components defines a refElement or refComponent
  2. A querySelector is done to find candidates
  3. Each candidate looks up in the tree for its parent data-component element.
  4. If ignoreGuard is passed, it will just return as valid
  5. find closest parent data-wrapper-boundary
  6. find the "owner" of the data-wrapper-boundary
  7. if the owner of the boundary is the same as the owner of the ref, return null
  8. if the owner of the boundary is the same as the closest parent (but is not the ref owner), ignore and find the next parent, and continue from 4
  9. if nothing is found, return null
leroykorterink commented 1 year ago

I think we can choose a better name instead of data-wrapper-boundary. What about one of these?