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

Component/attribute inheritance and precedence for primitives #5484

Open
mrxz opened this issue Feb 29, 2024 · 5 comments
Open

Component/attribute inheritance and precedence for primitives #5484

mrxz opened this issue Feb 29, 2024 · 5 comments

Comments

@mrxz
Copy link
Contributor

mrxz commented Feb 29, 2024

Description:

The inheritance and precedences of values for a primitive is currently as follows:

  • AttrValue (anything explicitly set on an element)
  • Primitive mappings
  • Mixins
  • Primitive defaults
  • Schema defaults

This is handled during entity and component initialization, but later updates can easily 'break' things. For example a resetProperty completely ignores mixins. Clobbering a multi-prop component wipes primitive defaults and mappings, yet retains mixins. There's a difference between initializing a primitive with a mixin and adding that mixin later. Some of these are fixed in #5474, but a lot of this logic for primitives lives outside of components. Effectively the precedence is:

  • AttrValue (incl. initial primitive mappings, mixins(!) and defaults)
  • Mixins
  • Schema defaults

Especially the way mixins are handled causes unintuitive inconsistencies like the fact that a mixin at init time on a primitive can result in a different outcome compared to that mixin being added post initialization.

However, there are benefits to the current behaviour, despite its inconsistencies. Because it's all combined into the AttrValue of a component, it allows the user to "subtract" from it. Take the following example:

// HTML
// <a-camera></a-camera>
cameraEl.removeAttribute('wasd-controls'); // Gets rid of the wasd-controls

Which is also documented and also an answer on StackOverflow. So it's a given that users do depend on this behaviour.

At the same time users might be surprised why the following behaves inconsistently:

// HTML
// <a-mixin id="blue" material="color: blue"></a-mixin>
// <a-box></a-box>
boxEl.setAttribute('mixin', 'blue');
// <a-image src="url(some-image.jpg)"></a-image>
imageEl.setAttribute('mixin', 'blue');

Only the box turns blue, while the image doesn't get "tinted" blue.

Personally I would be for enforcing the full inheritance at all times. This should avoid many inconsistent and unitive situations. But it won't allow any "subtracting", as in, the primitive can't be reduced beyond what it is by default. That would be a breaking change in behaviour, though.

@dmarcos
Copy link
Member

dmarcos commented Mar 5, 2024

A bit of a tangent. From the beginning I've considered primitives a liability and a source of complexity and headaches. It makes markup inconsistent in a model where we have tags that map to entities and their attributes to components. I definitively see the appeal of primitives for beginners <a-camera> <a-box>... it makes A-Frame very welcoming and concept of entity / components can be introduced later. Main reason I kept them around. Wonder if we should think about deprecating the public API (registerPrimitive) and keep only those relevant to beginners as a form of alias to the expanded entity + components.

We could rename primitive to alias to convey they're just a shorthand of the "real" thing. Maybe we wouldn't need to full support for mixins, arbitrary mappings... logic can be more ad-hoc for just the "aliases" targeting beginners.

@mrxz
Copy link
Contributor Author

mrxz commented Mar 5, 2024

Primitives have a nice benefit in terms of DX and readability, which extend beyond just beginner use. With only <a-entity> and its components, you'd have to infer its semantic purpose or name/label it accordingly. With editor based ECS engines, the outline will generally have the names followed by icons for specific components making it easy to see at a glance. The verbosity and structure of HTML makes this a lot harder. For example, compare the following:

<a-entity checkpoint geometry="primitive: cylinder; height: 1.5; radius: 0.2" material="color: green"></a-entity>
<a-entity checkpoint="final: true" geometry="primitive: cylinder; height: 1.5; radius: 0.2" material="color: purple" move-back-and-forth></a-entity>
<a-checkpoint color="green"></a-checkpoint>
<a-checkpoint final="true" color="purple" move-back-and-forth></a-checkpoint>

Of course there are multiple ways to achieve similar result. Mixins can be used or a component can be created that bootstraps the other components. Though, when working on a larger project, I find defining primitives to be a really nice way to make a sort of 'DSL' of tags.

The usefulness quickly drops the more you do programmatically instead of declaratively, but I don't see this as an argument against it. It's akin to how <div> tags can be made to behave like <img>, <button> or <a>.

Code complexity can be greatly reduced if the default values slot into the already existing inheritance behaviour. Having components without corresponding attribute is already the case for mixins, so dropping primitives won't help there. Only the attribute mappings are specific for primitives, but that is already handle in the primitive internally.

@dmarcos
Copy link
Member

dmarcos commented Mar 6, 2024

In practice when writing HTML or instantiating entities with JS you don't deal with the component expansion. The checkpoin component initializes the others at setup. A more accurate example of the dev experience would be:

<a-entity checkpoint="color: green"></a-entity>
vs
<a-checkpoint color="green"></a-checkpoint>

or in JS

var entityEl = document.createElement('a-entity');
entittyEl.setAttribute('checkpoint', {color: green});

vs 

var entityEl = document.createElement('a-checkpoint');
entittyEl.setAttribute('color', 'green');

The difference is pretty marginal if any at the expense of breaking the model consistency and introducing ambiguity (only <a-entities> in your scene graph with attributes corresponding to components). Also can be confusing when combining with other components because properties might be mapped to the primitive attributes resulting in collisions and unexpected results. Finally, usage of primitives is not very spread out past the built-in ones.

@mrxz
Copy link
Contributor Author

mrxz commented Mar 6, 2024

In practice when writing HTML or instantiating entities with JS you don't deal with the component expansion. The checkpoin component initializes the others at setup.

That's one of the alternative approaches I mentioned, but it isn't without its drawbacks. If the component naively sets the other components, it won't properly handle mixins or conflicting component values on the entity. Exposing all properties of the underlying components in its schema is tedious and couples the bootstrapping component tighter to other components than needed. All in all it would be very tricky to correctly implement the lifecycle hooks (init, update, remove), and everyone creating such components would have to do so.

This inheritance order is arguable a neat feature of primitives. The goal of this issue is to see if we can/should expand this behaviour beyond just initialization. That would allow users to add and remove attributes on primitives while retaining the base/default components and values of those primitives, which I think is generally more intuitive and expected behaviour.

The difference is pretty marginal if any at the expense of breaking the model consistency and introducing ambiguity (only in your scene graph with attributes corresponding to components). Also can be confusing when combining with other components because properties might be mapped to the primitive attributes resulting in collisions and unexpected results.

While I do agree that having attributes map to either components or properties is less consistent and a potential source of confusion (/ learning curve), the problem of collisions could easily be addressed by having attribute mapping collisions be handled the same way as component name collisions, that is, by throwing an error. Since primitives are instances of AEntity I don't really see why only having the <a-entity> tag would be inherently better. Assuming no collisions, they behave as such, so no code should have to care about the actual tag. And personally I find the readability increases if the tags reflect the semantics of the entity.

Finally, usage of primitives is not very spread out past the built-in ones.

Is that really the case? Both AR-JS and 8thwall provide primitives. Virtually all sky and ocean/water implementations offer a primitive. And GUI libraries like aframe-gui and aframe-material-collection do as well for their UI elements.

Primitives are ideal for those use-cases. Many users creating experiences instead of libraries will likely find no need to write their own primitives. But that doesn't mean they don't benefit from consuming primitives (built-in ones or third party).

@dmarcos
Copy link
Member

dmarcos commented Mar 6, 2024

I think there's not really a difference recommending <a-entity xxx> instead of <a-xxx></a-xxx>. Very marginal benefit to the user of primitives if any. Deprecating results in simpler more maintainable code, removes one layer of abstraction, less concepts to learn, less ambiguity and space for confusion. Makes also things more explicit even if sometimes more verbose. It's easier to understand what's going on.

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