Active-CSS / active-css

The epic event-driven browser language for UI with functionality in one-liner CSS. Over 100 incredible CSS commands for DOM manipulation, ajax, reactive variables, single-page application routing, and lots more. Could CSS be the JavaScript framework of the future?
https://activecss.org
Other
42 stars 7 forks source link

Component host auto-creation and the introduction of component event hierarchy #24

Closed bob2517 closed 3 years ago

bob2517 commented 4 years ago

Whilst playing around with events and components, it has become clear that assigning a scoped attribute on a host element in order to handle event references within a defined area has brought up a few issues.

For example, let's say you want to render 4 divs one after the other inside one component. You then render this component after a button.

The 4 divs do not have a host. Active CSS needs a host for components, so it assigns the parent element as the host. That way any events that are set up can be limited to the component without needing ids or classes. But the parent element is the body tag.

But because the scope is on the body, the body has now become the component host, so this means that the button is now inside the component. So any events referencing the button in the document scope are no longer found.

So something needs changing on this. I don't particular want to have the rule that every component must have a host, as this adds complexity when coding and is going to stump some people. One framework I know does this, but it shouldn't be necessary. Gonna ponder it.

bob2517 commented 4 years ago

What is the difference between this and a shadow DOM host? The shadow DOM host can only have one child - the shadow DOM. In Active CSS currently, you can have multiple components in one host. But this is where the problem is. We need a dedicated component parent.

The simplest thing is not to have an additional rule. A simple response to the above problem is to recognise that the parent element (eg. the body in the above example) now contains more immediate child elements that the top contents of the component (there's a button in there too - that was not in the component), and that a dedicated host element is now required to be inserted around the component, as that is the rule.

A limitation is that no host reference variables will be able to be referenced (unless these are added after the component is rendered - so a workaround is possible). So this solution is acceptable because if there was an actual dedicated host to the component set up, then we wouldn't need to insert a surrogate host in the first place.

So the rules become: 1) Components that need host reference variables must have a host element specified around the component. 2) Components that do not have a dedicated parent host will automatically get a surrogate parent tag inserted around the component. I think we need a special tag for this like <acss-scoped></acss-scoped> so we don't get enforce CSS implications beyond ">" usage for immediate child referencing. This is an acceptable workaround considering what we are trying to do. (If component functionality like this was ever ported into the browser, it could be better just to assign scope properties to each element as they were drawn rather than requiring a host - or maybe not - might have the same issues. But it would not be performant to do that outside of native anyway. If a page had a thousand elements and we had to assign scope properties on every element, we'd be there all day setting it up, and we'd have to monitor any new additions outside of Active CSS as well and do the same thing and everything would probably grind to a halt.) 3) Events for components in elements must be declared inside component declarations and not outside.

Hmmm... The only problem with this is that that last point invalidates CSS selection for event selectors. There must be a better way around this. This is the best solution so far though.

bob2517 commented 4 years ago

So let's looks at what we have so far: 1) Events declared in a component need a scope. Otherwise we can't limit the CSS selectors to a certain area. Eg. a click on a p tag in a component needs to only work on the component and not go outside the scope. 2) If the scope is assigned to the parent of the component but there is no dedicated parent, there is a potential for a p tag outside of the component but inside the scope to respond to the event. So we do definitely need a dedicated host element for all components, which is covered by the first 2 rules above.

With regards the 3rd rule in the previous comment, should global event selectors, like "p", still be able to work on component elements containing matching elements?

The answer is yes - if the event matches the CSS selector then it should run, regardless of whether or not it is in a component or not. Obviously per native rules global events wouldn't work inside a shadow DOM component, which is fine.

But if the event is private, are we implying that events are also private and we shouldn't use global CSS selector events? No - even shadow DOM events bubble up the DOM. That would be an addition to the DOM rules, and I'm not sure this is even a wanted thing at this point. It could be added later though - like an "isolated" parameter on the component or something to only run events declared in the component and no other. It wouldn't be hard to add in (if anyone thinks this would be useful then send an email to support).

So perhaps all we need to do, in addition to the first 2 rules above, is run all appropriate global events on the element as well as the component declared ones. This would bring it in line with inheritance rules.

I think that's all the points covered. That should restore intuitive coding for non-shadow component events without any additional rules changes bar the 2 top rules enforcements above. Everything would get weird if those rules were not in place currently anyway, so this is a fix rather than a breaking change.

bob2517 commented 4 years ago

Surrogate hosting is now in place for non-shadow DOM components that don't have a dedicated parent element. Just need to add the global event running for non-shadow components and that should be it for this ticket.

bob2517 commented 4 years ago

Just a note on this - no scopes are needed at all if it is not a private component or there are no events on a component. Variable scopes are inherited. So we only add the surrogate scope when it needs it. So basically, if this is a private scope, or there are events in the component, if there is no dedicated component host then one will be created (<acss-scope />).

bob2517 commented 4 years ago

Need to sort out nested event inheritance on the same element. Bubbling isn't the same thing. Dunno how I missed this. Probably easiest to store an array like component => parentComponent. While there's a parent component associated to a component loop over the parent, check for events, assign to the parent and repeat. Something like that. Then finally run the global events. Probably a good idea to have a prevent-event-default action command to go with it so the developer can stop an event from going up the chain.

[edit] Pondering this, I'm not sure this should be default behaviour. An event specifically declared inside a component should perhaps be isolated to that event alone, and not available to child components. Currently it works like that. I think more components need to be built, and if it comes up as missing functionality then it can get upgraded at that point to have a non-default inheritance option of some kind. Leave this and start a fresh ticket if and when this issue comes up for real. It just isn't clear at the moment because there aren't enough examples.

[edit the sequel] Pondering this even further, I'm pretty sure I got it right at the top of this comment. Event inheritance should apply following the axioms of DOM rules. Components appear to be to higher-up components/the document as child elements are to parent elements, so it follows that inheritance should occur for component events (as distinct from bubbling up the DOM elements, which is something else). A new "prevent-event-default" command can be used to stop inheriting events in a higher-up component, up to the global scope. This is new territory, so it needed a bit of thrashing out to nail it down. The only question remaining is whether shadow component events bubble up. Meh. Dunno. The developer can always do a prevent-event-default at the top of the component of some kind. You might want a shadow component but still have it be responsive to any higher event. I think just do the nested inheritance with prevent-event-default, and see how easy it is to fully isolate a complex shadow component.

[edit the triquel] The event hierarchy could have something similar to shadow DOM like open and closed. So the closed parameter could contain fully encapsulated events and not respond to higher up component events and global events, and the open parameter could allow response to global events and bubble up events into the next higher up component, etc. Note this has nothing to do with DOM bubbling. This is event inheritance, which is iterating over higher and higher events for the same element.

bob2517 commented 4 years ago

This ticket is actually more exciting than it sounds. Component event inheritance, as opposed to element bubbling, is a brand new concept and complements element bubbling. It will become more obvious once there's an example page setup on the docs site. In a function-based language trying to manage native event listeners for this sort of thing wouldn't be practical.

bob2517 commented 4 years ago

Just a note on this as a reminder - the component host stores the properties of the component. This needs to be borne in mind when processing events and inheritance (bubbling up events in higher components). Component hosts need to use the parent component events, not the component it references as it isn't in the component itself. Element bubbling now takes this into account.

Also remember that beforeComponentOpen and componentOpen are excluded from all component event inheritance.

bob2517 commented 4 years ago

With this upgrade it makes sense to have two new action commands:

1) "prevent-event-default", which stops event inheritance from the higher component. 2) "stop-immediate-event-propagation", which stops any further events from occurring after the current event selector declaration has completed.

These are in place and just need testing.

bob2517 commented 4 years ago

Almost done on this. I'm going to change it so that open and closed only refer to shadow DOM modes. Mixing these up with component event inheritance makes things inflexible. Like as it stands right now offline you couldn't have an open Shadow DOM component with isolated component events. Eg. a p tag click in a component would bleed up to the document level and run the p event from there. You might want all p tags to respond in a component-only way, and not bleed up to the document p tag handling. So there's a scenario uncatered for.

So there's going to be additional open and closed type parameters for the component event hierarchy. Haven't worked out what to call these yet. WIll ponder on this and come back to it tomorrow.

Functionality is all is place though - looking good. Got it down to a simple solution eventually.

bob2517 commented 4 years ago

Note to self: Drawn component references need fully cleaning up when components are removed from the DOM. Currently clean-up is limited to components with reactive variables, and that isn't cutting the cheese. So a clean-up should be part of the release for this to round it off.

bob2517 commented 4 years ago

Maybe "closedEvents" and "openEvents" as parameters. I think that's clear enough. Defaulting to "openEvents", so there's full selector inheritance by default.

So in this example both alerts display when the p is clicked starting with the component alert and moving up (note that the component rendering isn't part of this code - let's assume myComponent is drawn already):

p:click {
    alert: "hello";
}
@component myComponent {
    p:click {
        alert: "I'm in a component.";
    }
    html {
        <p>Click me</p>
    }
}

... and in this one only the alert inside the component runs because events are closed off - it doesn't inherit any events higher up the DOM, so the document-scoped p click event doesn't run:

p:click {
    alert: "hello";
}
@component myComponent closedEvents {
    p:click {
        alert: "I'm in a component.";
    }
    html {
        <p>Click me</p>
    }
}

And it works with nested components in the same expected way as above. That's it in a nutshell.

The same "openEvents" and "closedEvents" parameters do exactly the same thing with native shadow DOM components (which are written as above but with the additional parameter of "shadow"). So this new stuff here doesn't modify existing native shadow DOM behaviour in any way.

bob2517 commented 4 years ago

I'm going to simplify this to one optional parameter "privateEvents".

So there's currently now 5 parameters for the @component syntax: 1) shadow (draws component as a shadow DOM) 2) open (applies to shadow DOM component only - open shadow in "open" mode) 3) closed (applies to shadow DOM component only - open shadow in "closed" mode) 4) private (applies to all components - all variables are privately scoped to the component) 5) privateEvents (applies to all components - all events are privately scoped to the component)

I'm thinking whether to rename "private" parameter to privateVariables to keep distinction. Private could mean anything. I don't want to lump private event functionality in with private variable functionality as it's nice to have the flexibility of either/or.

So it would be "privateVariables", and "privateEvents"...

I think this would lead to less confusion. Otherwise it would be "private" and "privateEvents". So what does "private" do? It isn't specific enough now.

So I'll deprecate "private" and introduce "privateVariables" as a new parameter that does the same thing as "private".

So the new list is: 1) shadow (draws component as a shadow DOM) 2) open (applies to shadow DOM component only - open shadow in "open" mode) 3) closed (applies to shadow DOM component only - open shadow in "closed" mode) 4) privateVariables (applies to all components - all variables are privately scoped to the component) 5) privateEvents (applies to all components - all events are privately scoped to the component)

If there are no parameters and no events then no host element is enforced - the component is just a block of html.

bob2517 commented 4 years ago

This now all works offline, full ticket is complete and is ready to go live with 2.4.0. Note to self: docs need updating.

bob2517 commented 4 years ago

There's an issue with using a host reference attribute variable inside a non-private variable scoped, non-shadow DOM component. Basically there is no scope host or shadow DOM host, so there is no defined host. Sometimes there isn't even a parent tag to host to.

You shouldn't need to have a privately scoped component in order to make use of host attribute references. "Host" implies the immediate parent of the component. But it wouldn't make sense for the host to be the immediate parent element to a host reference variable, as its main use is within components - the host reference variable could be half-way down in a tree structure inside a component, so having the host as the immediate parent element to where the reference variable is being used is not the solution.

So the host definition is nailed down to: For a non-shadow DOM component, the host is the first developer-defined parent tag found from the outer component going up. For a shadow DOM component, the host is the shadow DOM host.

So for non-shadow DOM components: 1) For a privateVariable component that has a surrogate host (internally created - not by the developer), the parent of the surrogate is the host. Host reference variables aren't going to be useful on the surrogate host - they need to be set up by the developer. 2) For a privateVariable component that has a defined parent element, that is the host. 3) For a non-privateVariable component that has a defined parent element, that is the host. 4) For a non-privateVariable component that has no defined parent, the host is the first parent found outside the inner component, which could be anything - including the body tag.

This will keep it backward compatible too. This host use needed adjusting due to the imminent release.

Of course there's no point having a host element on anything other than a custom element for reactive variables, as you can only monitor attributes on custom attributes. But one off use of the attribute could work on any tag, so this is the correct solution.

Hang on a sec. What if the inner component host reference wants an outer outer outer component host, as all the inner components are minor HTML blocks? That's not going to work at all. But I guess a variable could be set on the immediate inner component to get the host variable.

I think there's an need, as well as this, to allow variable inheritance from outer components even if the inner component is private. But that will be a separate ticket.

bob2517 commented 4 years ago

Need to do training for work purposes for 2 weeks, so will have to come back to this then. But if anyone has an urgent bug fix requirement, gimme a yell and I'll try and squeeze it in.

bob2517 commented 4 years ago

Not quite done on the training for work, but I'm really missing working on Active CSS so taking a day off to get this issue wrapped up.

bob2517 commented 4 years ago

Finally got point 4 sorted out - and there may be a slight speed improvement in rendering multiple elements as a result of optimizing something:

"For a non-privateVariable component that has no defined parent, the host is the first parent found outside the inner component, which could be anything - including the body tag."

Once this has had another round of tests that should be it for this hopefully. Looking forward to getting component-based development technique fully sorted out, but a few more improvements with components need to be made before that can happen.

bob2517 commented 4 years ago

"For a non-privateVariable component that has no defined parent, the host is the first parent found outside the inner component, which could be anything - including the body tag."

This is almost there. It's not working when there are no events in the non-private component.

bob2517 commented 4 years ago

This idea of surrogate hosts won't work if the component is essentially a tr tag without a parent that isn't a table yet contains events. You can't wrap a tr tag in a div and expect a table to display correctly. A possible solution could be to allow multiple inner components to share the same component host. So an outer div could be a host to multiple inner components. This wouldn't work with a shadow DOM, but then the shadow DOM has to have a host anyway. This needs to just work. Adding additional rules for development is a bit of a cop-out.

bob2517 commented 4 years ago

Following on from that last comment, this currently breaks with a surrogate host as it wraps a div around it:

@component showThings {
    td:click {
        alert: "This table is broken.";
    }
    html {
        <tr>
            <td>{thing.first}</td>
            <td>{thing.second}</td>
            <td>{thing.third}</td>
        </tr>
    }
}

Same thing would happen with a sole select option tag inheriting attributes somehow.

bob2517 commented 4 years ago

The indicators are like "if the host is not obvious, then it is always the parent element immediately above the component." to solve it as a rule. That seems to be the most intuitive way to do it.

But that doesn't solve the scoping of events to the tr component above. Those events need to be contained to something. Unless we enforce the outer render element like React. I really don't want to do that though, as it's a bit of an arbitrary rule and something that prompts the ol' "but why?" response. And anyway, it wouldn't work with a component that contained several tr tags to be inserted into a table

I can see why shadow DOM components need a dedicated host - it does solve problems. There must be a better solution to this though, as it is not going to cut the cheese as it stands.

The indicators are that the elements themselves, or at least the top-level elements, need to be the thing that defines what the scope it. Currently the scope is set by the host's single active-id attribute. It could be that we need multiple references to the component all sharing the same scope. So the multiple active IDs could reference a single defined scope. That could work. So just create a new array of scopes, and have active IDs associated with that scope reference. I think that's the only sensible way forward.

That doesn't solve the host issue, but it solves the component containment of events. The host needs to refer to the parent of the top-level element inside the component in this case and be disassociated from the scope.

bob2517 commented 4 years ago

I'm going to solve this: https://github.com/Active-CSS/active-css/issues/24#issuecomment-699504343 before tackling the invisible scoping issue.

bob2517 commented 4 years ago

Note to self: it looks like as long as the component reference variable is shared somehow between each of the top-level elements in a non-hosted component then the fix for this should hopefully be fairly isolated.

bob2517 commented 4 years ago

The more I look at this, the more I don't know if this even needs fixing - it would be a complicated fix on a non-practical example so I'm not sure it warrants the total refactoring at this point. It might be simpler to have a rule like "to have events isolated to a component, it is necessary to have a dedicated host or parent element."

Gonna go with that for now and just fix the existing practical bugs related to this issue and see if any other real test cases present themselves.

bob2517 commented 4 years ago

The needed fix was always to scope a component when it is assigned to an element created with create-element. This should cover traditional use cases.

bob2517 commented 3 years ago

privateVariables renamed to privateVars on branch.

bob2517 commented 3 years ago

At some point during the implementation of setting up host attributes, the scoping of inner non-private variables within a private variable scope busted or at least has now been totally clarified if it wasn't completely working earlier. It's clear from working on the docs code editor component that sub-components must inherit all variables from all components above unless marked as private. Re-opening.

Also a distinction needs to be made for host attributes. There are two types of host attributes. One passed into a privateVars component, and one from the component host that is not a privateVars component. It needs to be very clear which one is being referenced. I'm trying to work it out in the core automatically and mixing the functionality at the moment, and it's making the core code confusing trying to work out which one to use. But most importantly, it isn't clear to the developer what will happen if the host attribute is referenced and what result will come back.

So I need the following functionality:

1) Sub-components need to inherit all variables from the component above unless it is marked privateVars. It used to do this before host attributes were "sorted out".

2) Host attributes need two variable references. {@host:var} should reference the component host, regardless of it's private status. {@privateHost:var} should reference the attribute in the uppermost scoped host of a private component. This should give clarity to implementation and to the developer and takes away the ambiguity.

Event scoping is something else entirely - it should always be limited to the component it is written in and is always scoped. Host scoping and variable scoping has nothing to do with event scoping and the separate scoping methods must be kept separate in the core.

bob2517 commented 3 years ago

Meh - I had a typo in my config. Maybe it does actually work. Need to work on the component some more to make sure... doh.

bob2517 commented 3 years ago

Host attribute needs to be available in preComponentOpen - currently it's only available once the component is drawn on the page.

bob2517 commented 3 years ago

Host var not evaluating in conditional in componentOpen.

bob2517 commented 3 years ago

Closing this issue. Gonna open a new one so I can focus on the steps needed to resolve all the issues.