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

Set up reactive (non-user interaction) events that use the ACSS conditionals (the "observe" event) #184

Closed dragontheory closed 2 years ago

dragontheory commented 3 years ago

Thank you for your hard work!

I am attempting to emulate native CSS :has() behavior with the below rule but it is not working. Seems pretty straight forward. What am I missing?

Passively (without end-user interaction) watch for:

#component div:if-empty {
    body {add-class: '.loading';}
}

CodePen here

Please advise.

Thanks again!

bob2517 commented 3 years ago

Hi there! ACSS doesn't work passively. It only works in response to events. That's the issue.... you need to have a :click or something at the end of each event selector. Check out the documentatiom on event selectors. Hope that helps.

Regards, Rob

⁣Sent from BlueMail ​

On 4 Oct 2021, 19:54, at 19:54, D7460N @.***> wrote:

Thank you for your hard work!

I am attempting to emulate native CSS :has() behavior with the below rule but it is not working. Seems pretty straight forward. What am I missing?

Passively (without end-user interaction) watch for:

  • Rule = TRUE Add the .loading class to <body>.
  • Rule = FALSE Remove .loading class from <body>.
#component div:if-empty {
   body {add-class: '.loading';}
}

CodePen here

Please advise.

Thanks again!

-- You are receiving this because you are subscribed to this thread. Reply to this email directly or view it on GitHub: https://github.com/Active-CSS/active-css/issues/184

bob2517 commented 3 years ago

Something like this as a workaround should work, but it isn't - I'll have to check this out after work this evening when I get more time.

div:draw {
    @if inner-html(#content div "") {
        trigger: populateDiv;
    }
}
div:populateDiv {
    body {
        add-class: loaded;
    }
}
bob2517 commented 3 years ago

Just a note on the initial thought before I shoot off to work - as the check should be passive the demanded functionality should be a part of the regular CSS spec rather than in ACSS which is purely user-initiated event-driven. The alternative would be adding some sort of JS observer to the div.

dragontheory commented 3 years ago

We are all busy. Thank you very much for your quick response!

I agree with and share your passion for CSS and the benefits of decoupling the UI from the data (Separation of Concerns).

This is most likely "delusions of grandeur", but I am attempting to take it the "last mile" by leveraging CSS's unique "plug-n-play" nature to passively watch for (without expensive polling) and react to (without end-user interaction) virtually any changes on the DOM, thereby COMPLETELY decoupling the UI from the data.

In other words, the UI doesn't react to end-user interactions. The UI reacts to the DATA being loaded from end-user interactions. Button clicks no longer control the UI. They simply fetch data.

Integration could be as simple as a list (data-schema/config file?) of anticipated selectors, text strings, and or pseudo element conditionals.

The UI would operate independent of various JS frameworks (or any other data delivery system) and intelligently passively watch for data points to show up in the DOM.

And since it lives and operates in the browser, no compiling or other tools are necessary. Back-end DEVs only worry about data. Front end DEVs only worry about UI.

Revolutionizing web development as we know it.

Not sure how far I can go, but it requires the use of, as of yet browser supported, CSS functionality (such as parent selectors and :has()), which your good work provides.

So, is there some way, using your script, to emulate passively watching for and reacting to CSS/ACSS rules as they become true and false?

For example, watch for all form elements within a given <div>​ to not be :empty() and then add one or more class(es) to <body>​ and remove those classes when that rule becomes false.

Thanks again.

bob2517 commented 3 years ago

Yes, I think you're onto something there. I've had a good think about it and it should be possible in a relatively performant way. I'll have time to look at it properly at the weekend. It's looking very possible though.

I did send some emails to this issue yesterday from my phone, but I don't think they arrived. The fix for the initial problem would be basically just adding a draw event and changing the conditional to a more appropriate one:

#component div:if-inner-html(self ""):draw {
    body {add-class: '.loading';}
}

But again - I've got to rush off to work so I don't know if that actually works or not...

dragontheory commented 3 years ago

That worked!

Updated CodePen: https://codepen.io/dragontheory/pen/VwWOJqd

.loading class is added to the <body> - assuming the init is on page load?

Another example: Expected native boolean CSS behavior would remove the .loading class when the CSS/ACSS rule is made false (programmatically or manually through the Element Inspector in the [ F12 ] Developer Tool) and added back again when the rule was made true again (programmatically/dynamically/without user interaction).

Greatly appreciate your time and consideration and look forward to what you come up with this weekend.

Pouring over documentation and have more questions but will save them for another time.

Thanks again!

bob2517 commented 3 years ago

I'm very glad it worked! I dunno why I was getting so complicated before...

.loading class is added to the - assuming the init is on page load?

Yes, on first loading of the page each element gets the draw event checked and ran if it is present. Point 3 on the initialization docs page: https://activecss.org/manual/init-events.html

I'm getting a bit excited about this new feature you've suggested. I can't actually start any coding on it until the weekend, but I've basically worked out the code in my head for it, assuming that it's even possible, which I think it is. There could be major performance hits if people get crazy with trying to observe a lot (eg. someone sets up an observer CSS rule that triggers an event over menu items that get a class added every time the mouse goes over the menu item so observer events happen 20 times a second), so it needs to be as quick as it can be and I think I've worked out the fastest method. I won't know until it's coded how much of an impact it's going to have on perfomance. But performance will only be affected if the observing method is actually used in the config, so there's no harm in experimenting with it.

Just to save some time on getting the code set up for testing, I'm going to add a new event ":observe", which will be on the end of each of these - for now only (probably) - like:

#component div:if-inner-html(self ""):observe {
}

... but only while testing is happening on it. Once we are sure it's working then I'll remove the ":observe" part and tackle the next issue of trying to get it living alongside regular CSS. So it will ultimately look like:

#component div:if-inner-html(self "") {
}

(The "if-" prefix is there just so it doesn't clash with CSS in the future when new native pseudo-selectors get added to the spec.)

Internally though (I'm noting to self on this now) - we'll insert the ":observe" event onto observing events so it slots nicely into the internal config array where all the config lives.

bob2517 commented 3 years ago

Just an additional note - custom element attributes can be "observed" reactively already in ACSS, but that's more of a web component thing that's only really useful when creating a shadow DOM component and you need to pass data into the component, so it's not really related to this concept you are talking about. I'm just mentioning that in case you stumble upon it in the docs.

bob2517 commented 3 years ago

And obviously there's reactive variables in ACSS, which sort of does it, but not at a DOM level with conditionals...

dragontheory commented 3 years ago

Wow. That sounds great! Exciting indeed!

There are probably less complicated ways to do this but here is one way this "live" technique could run the entire layout...

             [ visible on screen ] | [ off screen ]
                                   | 
  .app-nav ---> .app-results ----> | .app-details--->.app-chat
 /______ /_________________________|/______________ /__________
|  nav  |          results         |    details    |   chat    |
|  ---  |    --:----:-----:----    |   _|tabs|_    |           |
|  ---  |    --:----:-----:----    |               |           |
|  ---  |    --:----:-----:----    |               |           |
|  ---  |    --:----:-----:----    |               |           |
|       |    --:----:-----:----  <-|-------<-------|---<-------|
|_______|__________________________|_______________|___________|
                                 <-|--- Slides on screen when 
                                   |    classes appear in <body>
                                   |    triggered by loaded data

Again, there is probably a simpler way to do this but simple class sequences could be used to ensure sections of the UI can't suddenly show up randomly, out of order, or on top of each other.

For example:

  1. For <app-nav> to appear, .app-nav class must be in the <body>.
  2. For <app-nav> and <app-results> to appear, .app-nav and .app-results must be in the <body>.
  3. For <app-nav> and <app-results> and <app-details> to appear, .app-nav and .app-results and .app-details must be in the <body>.
  4. So .app-details can't appear without .app-results, etc.
  5. For ANY of them to appear, DATA and or pseudo element conditionals must match CSS (with ACSS parent selector assistance) rules to make them true.

While we're at it, a few more ideas for the UI to run itself based on loaded data and or visible/hidden elements (no user interaction necessary):

  1. Leverage intersectionObserver to determine how many results table rows are visible minus the total number of rows using <ol> <li>s (or CSS counter()) to accurately auto-update pagination controls on the fly in real time (hidden by default). They are just waiting for a number.
  2. Both screen widths on the Samsung Z Fold 3 are non-standard (not even close). I'm tired of keeping track of all the different break points for responsive layouts. Instead of guesstimating break points for responsive layouts, intersectionObserver could be used to calculate when visible elements begin to overlap and generate DYNAMIC breakpoints on the fly in real time.
  3. Scrolling is another big one...

Anyway, didn't mean to clog up your GitHub issues section. Sorry if this is too much. Just thought I'd share some ideas.

Like I said, "delusions of grandeur". lol

bob2517 commented 3 years ago

Wow - a lot of great ideas! I can see how this is going to reduce code even more than can be done already. Scrolling can be done with the scroll event currently, but not without several @if statements inside to check state of other things. It looks like these implementations mean that the @if logic can be decoupled from the event and placed with the element itself.

If you get more ideas (and you probably will if you're anything like me), either create a new issue (if you've nailed down some sort of syntax) or start up a discussion in the discussion section. That's what that section in GitHub was supposed to be for, but no one has suggested very much to date :)

Let's sort out the first point and get a working version on the latest branch and then we'll take a look at the intersectionObserver stuff after that.

bob2517 commented 3 years ago

I think this can all be summed up under the term "reactive events", or events that get triggered as a result of other things.

dragontheory commented 3 years ago

It's a different paradigm of thinking.

Ha! Got a feeling that's about to change... ; )

bob2517 commented 3 years ago

Into the "talking to myself" stage on development now, where I'll be putting things into the ticket which is relevant as it comes up - it doesn't require commenting on by anyone particularly - it's just better for me to write stuff down rather than keep it all in my head clogging up my thought process. Random comments start now...

For the record, I think that currently the only "reactive" events (non-user initiated events - or in other words events that happen as a side-effect of a user-initiated event) in ACSS are these:

draw disconnectCallback adoptedCallback attributeChangedCallback beforeComponentOpen componentOpen attrChange

Each one has its own handling in the core. And each one passes through the _handleEvents function to get their conditionals handled.

To these are being added the single "observe" event, which will be based on mutations covering all element mutation scenarios and thereby should all follow a single consistent observe rule on mutations, hopefully.

bob2517 commented 3 years ago

Note: disconnectCallback, adoptedCallback and attributeChangedCallback are web component events and haven't been tested a great deal, so they are not documented yet...

bob2517 commented 3 years ago

Initial development test code will be:

body:not-if-has-class(self .HelloWorld):observe {
    add-class: .loading after 5s;
    remove-class: .loading after 10s;
}

The end result should be the class "loading" appearing after 5 seconds and disappearing/appearing on the body tag every 5 seconds.

The event should fire on first page load once and pass with the result of adding the .loading class after a delay of 5 seconds. Then that action triggers the observe event again and should fail because the body now has the "helloWorld" class. Then after another five seconds the remove-class command kicks in and the observe event runs a third time and this time passes and starts the cycle off by adding the class after a delay of 5 second. And so on.

This tells us that we should have observe events ready to run prior to the body:init event on the page load and after the body:preInit (I think).

bob2517 commented 3 years ago

Actually... that's a good point on initialisation. Do we run through all the observe events at the initial draw stage? I guess so. They'll either pass or fail. It makes me wonder though if all the draw events should run before the init event, as the draw event is also a reactive event, albeit very specialised. I need to look at this - preInit should be used for variable type setups. init should be used at initialization stage prior to any DOM stuff getting triggered. Then observe, then draw (which could trigger observe events), then scroll (which can also trigger observe events). There could also be a "postInit" event added to the end maybe (which can also trigger observe events). I'll slot in the observe events to run prior to draw for now. I'll maybe add an "afterInit" or "postInit" event just to round things off at the end.

After that the observe event will run on a per element basis whenever something changes in the DOM.

bob2517 commented 3 years ago

Basically we need to run through all the observe events on each element at the start, otherwise we miss their first appearance on the page, which is the first reactive event, and we can't trap that until after the fact, as the core doesn't kick in until after the page is already drawn. Hence we need to run through observe events on page load to account for the first DOM change, which is actually just the DOM appearing on the empty page.

bob2517 commented 3 years ago

Essentially, there is an draw event implied just by using the observe event.

Like here:

#component div:if-empty {
    body {add-class: '.loading';}
}

When do you expect this to run? The answer is when the element is first drawn and subsequent times when the DOM changes. I think that would go for any observe event. Will try out this theory and we'll see if it works.

bob2517 commented 3 years ago

I think this was why I went complicated on the solution to solving that in existing ACSS code - the draw event was hidden inside the fact that it was an observable event, but it is there, and using the draw event triggered the event and got it working as expected..

bob2517 commented 3 years ago

This also implies that observe events should also happen prior to draw events at the point of dynamic rendering the element on the page. So I'll put the observe event there too.

Which comes first, the draw event or the observe event?

Which would make more sense? Do we run the draw event and then manually trigger the observe event or do we let the fact that the element has appeared in the DOM trigger the observe event?

I might then take the draw event out of where it is now, and turn it into a mutation handling.

Should it run prior to the observe event? Probably, I think. Prior to the draw, it doesn't really exist, ie. it hasn't been "drawn" per the draw event.

Ok - I'll move the draw event into the mutation area and get this running before then checking for the observe event.

So, steps to implementation:

  1. Run the observe event on all elements after the draw events get run at initialisation time.
  2. Get the observe event working so we can see that the concept works.
  3. Put in a draw event to run prior to the observe event in the mutation section, so that should give me two draw events happening at this stage. This should be synchronous to ensure we get the "draw first, then observe" sequence.
  4. Remove the "manual" draw event calls.

That should be it.

This gives us the stable information that the "draw" event should always run before any "observe" events. The running of the draw event tells us that the element is there and can now be observed. It can't be observed before the draw event has run because it hasn't been drawn yet. The forced running of the observe event after the draw event will pick up any changes that happen after the draw event has been run.

There is a flag on each element that currently gets set when an element is drawn. This will be checked for mutation changes and elements will skip over observe events if this draw flag isn't set, so we can be sure that draw events always happen before observe events.

The last step before release would be adding any useful conditionals (DOM change pseudo-selectors) that might be missing.

bob2517 commented 3 years ago

There will be a double observe event if the draw event triggers a change to the element itself which triggers a separate observe event prior to the general observe event check. Not sure of the full implications of that yet, other than the fact there will be a performance hit. Will find out soon enough.

bob2517 commented 3 years ago

I just hope that the performance hit isn't too big. That is the only concern I have with all this...

bob2517 commented 3 years ago

I'm changing the test code for this to this:

#addClass:click {
    body { add-class: .loading; }
}

#removeClass:click {
    body { remove-class: .loading; }
}

body:if-has-class(self .HelloWorld):observe {
    p { render: "The helloWorld class is now on the body tag"; }
}

body:not-if-has-class(self .HelloWorld):observe {
    p { render: "The body tag does not have the helloWorld tag"; }
}
<button id="addClass">Add helloWorld class to body tag</button>
<button id="removeClass">Remove helloWorld class from body tag</button>

Something like that. It's just a bit quicker to see what is going on with buttons there... plus the code is a bit easier to read.

bob2517 commented 3 years ago

Step 1 is implemented, and the p tag now reads "The body tag does not have the helloWorld tag".

Speed is good, but the mutation observer stuff hasn't been done yet...

There is already a thorough mutation observer already set up in the _nodeMutations script. So I'm just going to add to that and see what happens. Fingers crossed on the performance...

bob2517 commented 3 years ago

Doh, the test code was wrong:

#addClass:click {
    body { add-class: .helloWorld; }
}

#removeClass:click {
    body { remove-class: .helloWorld; }
}

body:if-has-class(self .helloWorld):observe {
    p { render: "The helloWorld class is now on the body tag"; }
}

body:not-if-has-class(self .helloWorld):observe {
    p { render: "The body tag does not have the helloWorld tag"; }
}

I've got a basic implementation in place which works. There's not a lot of code there and it looks too simple - needs more testing, like when things get removed, added, blah. Test code works though, which is nice.

bob2517 commented 3 years ago

Yeah - just spotted that it probably won't work as expected with components. Will test the speed first, without my console logs, just to see the performance hit.

bob2517 commented 3 years ago

I'm not seeing any performance hit yet, but it's possible I need to set up more stuff. Will find out shortly.

bob2517 commented 3 years ago

I'm going to leave the observing of individual text nodes for now. There isn't anything set up in ACSS for handling individual text nodes, so there isn't much point implementing it yet. Not sure it's even needed - would need a use case for it.

bob2517 commented 3 years ago

I'm going to get food and call it a night. All looking good so far, although the ease of implementation does worry me a little bit. I should have something up on the branch by the end of tomorrow which can be tested in earnest...

bob2517 commented 3 years ago

I just realised my test case was over-engineered. The actual code you need is this:

#addClass:click {
    body { add-class: .helloWorld; }
}

#removeClass:click {
    body { remove-class: .helloWorld; }
}

body.helloWorld:observe {
    p { render: "The helloWorld class is now on the body tag"; }
}

body:not(.helloWorld):observe {
    p { render: "The body tag does not have the helloWorld class"; }
}

Basically just straight CSS with the observe event - and it actually works.

I think based on this that the "observe" event should stay as permanent syntax.

Boom! Problem of differentiating between ACSS and CSS... SOLVED!

bob2517 commented 3 years ago

More to do to get this theory working - currently the observe event will fire whenever there is a change to the element, regardless of what that change is, if it has the helloWorld class. This isn't exactly what we want. The example implies that whenever the helloWorld class is added or removed then the event runs, which is a different thing. The intended behaviour is in our heads rather than in the code itself.

I think the solution lies in the use of the event itself - we need a more flexible way to determine when an element will react. More events probably. Will ponder and resume tomorrow.

This works great as an event that triggers every time an element changes in some way though, so it's definitely a start.

bob2517 commented 3 years ago

Yes, like the DOM has lots of user-initiated events, we need lots of different reactive events. I'm pretty sure that is the answer. Just need to work out what those events should be...

bob2517 commented 3 years ago

But the problem of differentiating between ACSS and CSS is still definitely... SOLVED!

bob2517 commented 3 years ago

Unless there is a way of internally monitoring the state of all the reactive pseudo-selectors. That's an alternate solution. That would require setting the initial states of elements according to existing conditionals and then running events only when that state changes.

Performance hit? Possibly. Could get around it by having events, but it's not as simple as solution for the developer as simply working out what they mean from their code and doing the thinking for them.

Need to ponder it a bit more... will ponder this internal state method as that is definitely closer to the original syntax demanded.

bob2517 commented 3 years ago

And I still think that the problem of differentiating between ACSS and CSS is still definitely... SOLVED!

Eg. body.helloWorld:observe {...} still requires the event in order to distinguish the syntax from regular static CSS. So the observe event is looking like it should stay.

bob2517 commented 3 years ago

Hang on a sec... I think I may have cracked it. Will do some coding.

bob2517 commented 3 years ago

Going to do the internal conditional state tracking method. That is the way, I have spoken. I don't know at this point if more syntax is needed for the developer, but this is the next logical implementation step. Will see shortly if more is needed for the logic.

bob2517 commented 3 years ago

What makes this such an interesting problem is that mutation observers need to work on a cross-element basis. So if an element changes somewhere in the DOM and triggers a mutation event, there may be an observe event set up on any other element which needs to react accordingly. And it needs to work with both regular CSS syntax with the observe event and also ACSS conditionals. It isn't going to work to set up individual mutation events for each possible action - that definitely isn't going to work on a performance level. It won't be blazingly fast if I implemented it that way.

But... I think I've vaguely got the logic for doing this more sensibly, but again - this is where the performance hit may come in. Will see shortly.

bob2517 commented 3 years ago

The observe event needs to be able to run on an element that isn't getting observed, like here:

p:if-inner-html(#contentArea ""):observe {
    render: "Content area is empty";
}

The mutation happens on the #contentArea - not the p tag. Having an observe event on an element is disrelated to the actual mutation event - it needs more code to tie into the config events.

This an important point. It means that any mutation event on the page needs to be checked against all appropriate observe events to check if something needs to update. This is the potential performance hit.

bob2517 commented 3 years ago

Once the code is set up for this, then the intersection code can follow the same code logic.

bob2517 commented 3 years ago

I need to write this down as I've not woken up yet:

1) mutation event 2) scan all observed events for pass or failure 3) store the pass or failure state between the observe event and the element 4) run the event if there was a change and there was a pass.

All we want to know is if there's been a change or not - it doesn't matter if the mutation changed something else. We just need to know if there's been a change to the page. There may be a later observe event being scanned that is more applicable to an element, but the observe event won't get run twice because it's just been run and passed.

Something like that - the logic probably needs tweaking.

bob2517 commented 3 years ago

This is similar to how the main core event handling works and the performance is just fine, so hopefully this won't add too much overhead.

bob2517 commented 3 years ago

Ideally the code should be run in the _handleEvents script so we don't get double checking of conditionals, but I'm going to keep it separate for now so I can see what can be merged in and what can't - it may need a fresh event handling script to stop things getting complicated in the core.

bob2517 commented 3 years ago

I think this method will be ok in terms of performance. Mutations get grouped together before hitting the observer function. I just need to know if there was a change - there may have been a hundred changes but I only need to run the handler once, so that should mean performance hits are kept to a minimum.

bob2517 commented 3 years ago

I'll get this working at the parent DOM level, and then look at implementing it for shadow DOM components. That will be a whole different brand of fish. The target for today is to get it working at the parent DOM level.

bob2517 commented 3 years ago

We've also got custom selectors that can have conditionals, like this:

~myEvent:if-inner-html(#contentArea ""):observe {
    p {
        render: "The content area is empty.";
    }
}

Should this be able to be observed? I don't see why not. It just needs the ACSS conditionals checked.

That opens things up quite a lot, potentially. You could have instructions that aren't even tied to elements.

bob2517 commented 3 years ago

Gonna leave custom selectors for now. Will tackle regular element observing first.

bob2517 commented 3 years ago

Note to self: this is a "probably" - shadow DOM component mutations may or may not need a separate mutation observer, if that's possible. They shouldn't react to document level mutations, otherwise it defeats the point of the shadow DOM. There's already a way to tie shadow DOM changes to host attributes and that should be the only observable factor as that matches the way the browser works. So observables should be limited to document or shadow DOM scope. The same goes for private event scoped ACSS components, but will tackle these one at a time later on.

bob2517 commented 3 years ago

I think I've got it working at the document level for observing regular CSS selectors changes and also when ACSS conditionals are used. I've tested two scenarios so far, including elements changing that are used in the ACSS conditionals with no direct DOM change effect on the element being observed. Performance is fine I think. Cross-element DOM changes seem to be fully supported.

Still to do before I'm happy about signing it off:

1) Test with custom selectors - ie. ~madeUpSelector:if-inner-html(#myDiv ""):observe {} - the code is there, just need to set up a test case. That should be interesting, as it's a raw observe on the DOM using only conditionals that change in the DOM without a particular element being observed. Should still work in theory. Not sure the potentials on that one at this point, but it feels like a powerful feature.

2) Set it up to work on shadow DOM components.

3) Set it up to work correctly with ACSS components.

Then I'll do a commit and it can be tested in earnest from the branch.