blakeembrey / free-style

Make CSS easier and more maintainable by using JavaScript
MIT License
707 stars 29 forks source link

Element queries #9

Closed m59peacemaker closed 8 years ago

m59peacemaker commented 8 years ago

I just stumbled across this awesome library. I started writing something with the same goals in mind, but I am not as strong a developer, so my implementation is probably pretty weak. There is one concept I'm sold on that hasn't been solved yet and that is element based queries rather than media queries. I have contributed to element-resize-detector and used it in my library to shim element queries. I like the idea of being able to write styles like:

[{
  color: red;
}, function(element) {
  if (element.clientWidth > 1200) {
    return {
      color: red
    };
  }
}];

With my library, this works. Is it feasible to introduce this into free-style via some kind of plugin system, with a function wrapper, or some companion module? I'm not sure how to approach it. I would be glad to give something a go if given a little direction.

Here's my library for reference (don't laugh, or do laugh. I don't mind): https://github.com/m59peacemaker/js-style

blakeembrey commented 8 years ago

@m59peacemaker Awesome! This is definitely a useful utility to have, but I think the best way to interact together would be to return the class names for different element sizes. That way, instead of tightly coupling your solution with just free-style, it'll work with any CSS classes. You could also make it declarative and have a syntax more like:

var style = new ElementQuery({
  base: base.className,
  query: {
    '> 1200px': desktop.className,
  }
})

That's just a random idea, but using class names instead of styles themselves is much nicer and allows the styles to continue being computed upfront. The implementation itself likely resolves around whichever framework you are using to hook into element lifecycle management.

Does that make sense? I'd be happy to work with you to figure out the final API, I'm just giving some initial pointers right now.

m59peacemaker commented 8 years ago

There are so many factors to consider... I went with a custom attribute and mutation observer to make it more general, but that brought along issues for sure. I think your approach of generating a classname that hooks the style on makes a lot of sense, though it works against my goals a bit. Thinking in terms of classnames makes the developer think about an implementation detail and not just styling an element. Applying the philosophy of functional programming I've picked up from React, Cycle, etc, I think having a function that receives state and returns a style object is the most fundamental necessity to making style simple and powerful. That was the starting place in my library. So, the idea is that there is at least an initial call to that function to get a style object, and then the element resize detector calls that function when it needs to update. It's just so easy to reason about.

This is the API I would love to be able to use:

const styler = require('magical-styling-thing')(config); // options to listen for element queries, etc
const style = function(params, element) { // where params contains state from the view
  // use the params and element to form and return an object of styles to be applied
}

function view(state) {
  return h('input', styler(style(state), {type: 'text'})) // not sure how `style()` could get the element reference
}

And, in this example, the styler function could translate the style declaration into whatever property is needed, whether that be the style attribute, classname or something else.

For example, the result might be h('input', {type: 'text', className: 'abc'})

Because that API is based on a function, developers could declare styles just about however they want to and have a helper function do some extra translation. I like i-object, of course. :)

Thanks for interacting about this. I'm excited to see what we may think up.

m59peacemaker commented 8 years ago

Oops. I got carried away in that last post. Maybe not so relevant to free-style. I'd be glad to move that conversation somewhere else pending your thoughts. Or maybe these are ideas for free-style@2.0? :)

blakeembrey commented 8 years ago

I absolutely agree (sorry for a slow response, was meant to be on a break). It's not so much thinking about class names, but making it generic enough that it accepts class names. I don't think there's anything stopping you from using style objects, just know there's a trivial overhead with that implementation (could depend on the number of style transformations you intend to do). For style objects, you could just apply them inline.

On top of that, free-style@1.0 is going to (probably, still got to double check my thoughts after some months) only return strings instead of classes from now on. That abstracts it a bit more, as you'll just have const myStyle = style.registerStyle({ color: 'red' }) and then you can apply myStyle as the class name in whichever implementation you use.

However, you might not even have a need to bind to free-style with your implementation. One of the reasons for free style (to me) was CSS in JS, but also in a "universal" way - small and repeatable styles that are consistent across front-end and back-end. Since your behaviours all rely on JavaScript, using only objects makes complete sense and is more of a complementary project. I love it!

I'm not really familiar with the syntax above, but let me try something for you.

const styler = require('magical-styling-thing')

const style = function(element) {
  if (element.clientWidth > 1200) {
    return {
      color: red
    };
  }
}

function view(state) {
  return h('input', styler(style, className, { color: 'red' }))
}

Notice how "styler" can just combine styles and expand functions. Works with free-style and anything else out there. Functions can be applied using the same recursive logic where you could even return an array. Anyway, the harder thing about the function approach here is that you won't know when to reflow the styles. E.g. when does the element need to re-evaluate the width? That's when the declarative approach helps since dependencies are registered upfront. An alternative is to pass in an object (not the element itself) with helpers which then register the dependencies at runtime (E.g. if (params.clientWidth() > 1200) knows that clientWidth is a dependency and should re-evaluate on change).

m59peacemaker commented 8 years ago

Sounds like I just need to hack away with different ideas and see where it goes. I'll let you know if I discover something interesting.

blakeembrey commented 8 years ago

That would be awesome, cheers! You're welcome to link to whatever you come up with here and collaborate on the idea.