zurb / foundation-apps

The first front-end framework created for developing fully responsive web apps.
http://foundation.zurb.com/apps
MIT License
1.58k stars 217 forks source link

How can we improve settings? #23

Closed gakimball closed 9 years ago

gakimball commented 9 years ago

Right now we're looking at which of the hot new Sass features we want to use in Foundation for Apps. We'd like to shoot for libsass compatibility so our stack can be pure JavaScript, so when we say "new features" we mostly mean "maps". Maps aren't yet a native libsass feature but there is a polyfill for them.

Currently, frameworks spill out all of their variables like this:

$button-bg-color: #000;
$button-text-color: #fff;

Maps give us an opportunity to bring more structure to our settings file, by allowing us to craft an array of settings:

$foundation: (
  button: (
    background: #000,
    color: #fff,
  ),
);

Because of the confusing-ness of the map-get() function when accessing nested maps, we'd abstract map access out to a single foundation() function.

// The old way
color: $button-bg-color;
// The new (?) way
color: foundation(button, color);

More complex components could separate their settings by component.

color: foundation(dropdown, menu, color);

What we want to know is, does this make sense? Will users understand it? As far as how we write the components, we plan on giving users the same amount of customizability they're used to in Foundation 5. As usual our designs will be un-opinionated so you can easily overwrite them yourself.

Here are the benefits:

Here are the downsides:

Arguably it doesn't make memorization of settings any harder. You're still going to have to look up the really specific ones. However, there are a few that people use a lot in development.

padding: $column-gutter;
padding: foundation(grid, gutter);

color: $primary-color;
color: foundation(color, primary);

So those get slightly more obfuscatory, perhaps. It's a matter of re-training people mostly.

And as always we're going to try to balance best practices with ease-of-use for our users. If you went and asked any programmer, "would you rather list a bunch of related things as their own variables, or put them all in array?" the answer would be obvious. It makes sense to put things in arrays from a best practices perspective. That doesn't mean it's what's easiest for our users to understand necessarily.

But that's what we're trying to find out. Let us know what y'alls think!

Addendum

To expand on this slightly, we don't necessarily need to contain everything inside one $foundation variable. We could have separate variables for, say, component settings, global settings, breakpoints, and colors. The question is then how you access them.

// With the same function?
color: foundation(color, primary);

// With a unique function?
color: color(primary);

// Or should it be namespaced?
color: f-color(primary);
KittyGiraudel commented 9 years ago

Hey. Allow me to give my opinion here since you asked for it.

Let me break the ice to get things started: people don't think maps are weird. It's been months if not a year since maps have been first introduced to Sass so it's safe to tell things are well set up at this point.

So the question isn't "should you be using maps ?" but "what for ?".


What is interesting with having a map (or maps), is that you can loop over it, which is obviously not possible with variables. Actually, that's the main reason why maps have been introduced to Sass in the first place: to provide a solution to variable variables.

Having an ability to loop over a map is a huge plus when you come to build a living styleguide or basically anything that comes around the project (documentation, exports, ...). It's much more convenient than having a handful of variables to move over.

Also as you said, and I fully share your opinion, maps make your code more structured. For instance, this is what our _variables.scss file looks like at work. Definitely easier to read than countless variables in my opinion.


Let me warn you against having a super map. This is indisputably a bad idea.

For starter, deep maps can be a pain to update (reading is and will always be the easy part). Updating a 2-levels map is a pain to update manually. Needless to say building a function to dynamically update a nested map is a nightmare...

Moreover, deep maps tend to be slow. Everything is relative at this point of course but very dense maps can bring some slowness to your app. I'd warn against having more than 3 or 4 nested levels, it's probably too much.

Instead, try breaking your map into several smaller items. For instance, instead of:

$foundation: (
  button: (
    // ...
  ),
  carousel: (
    // ...
  ),
  dropdown: (
    // ...
  )
);

... try something like this:

$foundation-button: ( ... );
$foundation-carousel: ( ... );
$foundation-dropdown: ( ... );

Not only does it avoid having to deal with deeply nested maps, but it also allows you to move each configuration to its own relevant partial rather than storing everything in the same file (which may or may not be what you want).


Again, I think I'd advise against using a single function. Actually the name is the biggest flaw here. foundation means nothing (sorry). Pick something that actually makes sense, like map-fetch, then alias it with custom functions. Like so:

/// Helper function
/// @access private
/// @param {Map} $map - Map to parse
/// @param {Arglist} $keys - Series of keys
/// @return {*} Value at `$keys...` in `$map`
/// @todo Write input validations
@function map-fetch($map, $keys...) {
  @each $key in $keys {
    $map: map-get($map, $key);
  }
  @return $map;
}

/// Get color from palette
/// @access public
/// @param {Arglist} $kets - Series of keys
/// @return {Color}
@function color($keys...) {
  @return map-fetch('color', $keys...);
}

// Usage
$foundation-colors: (
  "primary": (
    "light": lightgreen,
    "normal": green,
    "dark": darkgreen
  )
);

$color: color(primary, light);

Consider using f-color or foundation-color (although that's a bit long) to avoid any naming conflict. If you want to stay consistent and avoid any possible confusion, you might want to consider using get-* name (e.g. get-color()).

While multiplying frameworks isn't a good idea, using external libs can totally happen. You don't want those libs to mess with the framework, do you?

My 2 cents. If you need any more help, be sure to ask.

xzyfer commented 9 years ago

We use maps for configuration in our internal framework and it's been incredible. We started off with something similar to what @HugoGiraudel suggested with f-color but found this didn't scale out to well, and lead to developer confusion. In the developers mind they set all their config in one map and got confused/frustrated around needing multiple sass functions to extract those settings. In the end we opted for a rather naive get() function that added sugar over sass' map-get().

I'm happy to elaborate further on how we currently deal with settings maps now.

More importantly I wanted to point you at https://github.com/sass/libsass/pull/481. Map support will be landing in libsass shortly, largely because it's so important to how we manage our sass settings map.

Undistraction commented 9 years ago

@HugoGiraudel Whilst I agree with a lot of what you've said, I'm interested in the following:

Updating a 2-levels map is a pain to update manually. Needless to say building a function to dynamically update a nested map is a nightmare...

I've not found this at all, so I'm wondering what kind of edge-cases and strangeness I'm missing. Sass Maps has a map-set-deep function which I've found effective and it's relatively trivial to write a function to recursively merge two maps.

KittyGiraudel commented 9 years ago

@Undistraction Did you have a look at the function you are talking about? It is almost 40 lines of logic, index manipulation, and conditional statements.

I wrote one myself a while back, and couldn't come with something much better, only slightly more readable.

Anyway, deep setting is a pain because of the way Sass works. You cannot update a map. You can just create a slightly different one.

Undistraction commented 9 years ago

@HugoGiraudel No arguments from me on the clunkiness of the logic, and the immutability of maps is a big pain the arse. I think in many ways, both are reflections of the strange place Sass is at. Personally I keep flipping between wanting to use it minimally and wanting to push it as far as it can go. After working in JavaScript or Ruby I come back to Sass and doing anything beyond simple functional stuff seems ludicrously awkward. And given that an awful lot of designers coming from CSS have no programming chops at all, their exposure to the clunky functional elements of Sass is not an ideal first experience. Don't get me wrong; I love Sass, but it seems to be growing into a strange frankenstein's monster.

I guess my point is that most designers using Sass don't care about the implementation of things like map-get-deep. They do care about simple tools that make writing CSS easier. If a multi-dimensional map is easier for the average designer to understand or interact with through a simple abstracted API that hides at least some of Sass's less pleasant functional aspects, then that is the best option. Though I'm currently up-in the air on the matter and I find deeply nested maps can be pretty confusing compared to a stack of well named variables.

KittyGiraudel commented 9 years ago

If a multi-dimensional map is easier for the average designer to understand or interact with

I really don't think an insanely huge map is any better than small ones, in any way (readability, maintainability, simplicity, and a buch of other words ending with -ty).

Undistraction commented 9 years ago

@HugoGiraudel Obviously an 'insanely huge map' is not a great idea, and I never suggested it was, but storing related values in a single location has a lot of advantages, for example debugging and looking for duplications/optimisations. The way I get around this issue is to store the values in a large map and then have a function/mixin that allows me to register elements to that map, for example:

@include register-palette(brand,
  (
    primary: $dark-blue,
    secondary: $mid-blue,
    tertiary: $light-blue
  )
);

That way I only need to think in small chunks but everything is stored in a single place. With some intelligent merging allowing for optional overwriting of existing values, I've found this a really flexible solution.

benfrain commented 9 years ago

I have tried this both ways. I find myself in agreement and disagreement with Hugo at the same time.

As @HugoGiraudel says, I've found enormous utility in maps for looping over things. Big list of colours you want to spit out into rules – perfect! Couldn't be without maps in that scenario.

However, when it comes to variables for colours and z-index, I'm yet to be convinced (although I welcome any further input) they offer anything for the end user - what are the actually buying you? Instead of

color: $color-grey-66;

You write:

color: color(grey, 66);

I fail to see how that makes life any easier for the author - they just want to pick the right colour and this seems a needless complication.

A good naming convention for these variables seems to get me just as much and makes them easier to work with. If I want to lighten($color-grey-66, 10%) - that's far easier to grasp than the map based alternative.

Having the variables as merely variables also makes things more portable if the project ever needs to switch to another pre-processor.

Great discussion so far though - keen to hear more :)

KittyGiraudel commented 9 years ago

I fail to see how that makes life any easier for the author

All colors are gathered in a single entity rather than existing individually. It's a matter of preference at this point. Also, using a function allows you to do some error handling (which is discutable since Sass basically crashes when referencing an invalid variable), deprecation, and what else.

Undistraction commented 9 years ago

@benfrain I think in that example, pulling colours from a map is pointless, because you get nothing from it you don't get from variables. When you abstract the colour names to something more semantic though is when this kind of thing becomes interesting. However, I'm not convinced that just having a button (for example) do button: palette(button, color) is much better. It still atomises the colour configuration so that maybe button, tab, badge and whatever else are all making separate calls to pull in the same colour value. This does make more sense if there is a system in place to inject theme values into all components so that this duplication becomes a product of the theming, not a maintenance pain.

To be honest, I'm feeling more and more that breaking out colours like this is fine for examples of what can be done with Sass maps, but is really only part of a wider issue. If we are going to configure colours, then why not background images, border-styles, text-shadow and other decorative styles (as opposed to behavioural or layout styles) that are widely used in theming components. What I think everyone is looking for is a way to theme every component in a project and colour is just one strand of this. Why should setting a background-color be treated any differently than setting a gradient background?

I also think if you are going down a settings route, media queries have to be factored in. This can be true of decorative styles and is absolutely true of layout.

gakimball commented 9 years ago

You guys are the best, thanks so much for your input! To respond to a few points brought up here:

Using maps for their map-ness: As Hugo and Ben said, the main reason we'd want to use arrays is to loop through their values, or perhaps be able to call variable variables. We're definitely using maps to make managing breakpoints more flexible; it makes outputting those small-, medium- etc. classes more manageable. I had also messed around with putting CSS properties in maps and then outputting them with a mixin, but it seems too restrictive. The map can't reference other values in the map because it's not done being created yet, you can't add mixins, and so on. Maps have also been useful for a lot of little things, like outputting values in mixins based on user input.

The thing we're getting hung up on is where using maps gets us structure, but not utility. A few of you touched on this point. Ultimately grouping all button settings into one variable is nice because it gives our code a little more structure, but we would never need to iterate through the values of that map, and we would never need to dynamically access any value in the map. So that means the map exists purely for structure.

The map accessing syntax: As a few of you mentioned, the difference between $button-background and foundation(button, background) is almost nil in terms of remembering, but perhaps our audience will better understand what's happening with a variable. The average user of Sass probably understands (or in my opinion, only needs to understand) nesting, variables, partials, and maybe mixins and a few other things. Those core concepts get you most of the awesome functionality out of Sass, and they're relatively easy concepts to pick up. If you're a library author, then you understand all the more complicated bits, like functions, lists, maps, and so on.

We don't usually assume our audience understands the complexities behind our tools. (And that's not meant to be an insult, we just want our framework to be accessible!) The theory behind tools like Foundation and Bootstrap is that you can prototype a website without writing any CSS. With Foundation for Apps we're taking that a step further: you can protoype a single page web app without writing any JavaScript. Abstracting complexity is the name of the game here. Of course, it's not cutting advanced users out of the loop; if you know Angular you'll be able to take something from static prototype to functioning web app.

And that brings us back to the point that the map access syntax is awkward. By using maps in this library we're passing along a bit of the complexity of maps on to the user in some small way. Of course they don't have to directly manipulate maps, but they have to access them using the map syntax, which is awkward, even for people who know what they're doing. I think accessing arrays in an entirely functional manner is just super weird, but I guess I understand why Sass was written that way.

There are also some little things that make maps a hassle. For example, we often have variables that are interdependent to make theming a little easier.

$button-background: $primary-color;
$button-text-color: if(lightness($button-background) > 50%, #000, #fff);

You can't do this in a map, because you can't reference another place in the map while it's still being defined.

$button: (
  background: $primary-color,
  color: map-get($button, background), // Nope
);

The solution we land on will probably be a more balanced approach to the "super map" I was discussing initially. If we use maps for components, each component might get a separate map. There's still an issue of access, though, and I think this syntax would be really awkward for users:

color: map-get($f-button, color);

Also I'm now going to try to wrap this up so I can stop typing. We're trying to figure out a solution soon, so just picking one will be more useful than our team endlessly speculating about different solutions. It doesn't even affect how we develop our components that much, it more affects how our users manipulate them. And I don't know that our users have ever run into problems with our variables interfering with other libraries, so if we just stuck with the "old way" of a million variables in a well-organized list, I don't know that people would complain.

But thank you so much @HugoGiraudel, @xzyfer, @Undistraction, and @benfrain for giving your input!

AshleyAitken commented 9 years ago

Would it be worth doing both, i.e. having maps and structured variable names? The best of both worlds with a small price to pay in size.=

Undistraction commented 9 years ago

@gakimball I think you're bang on the money when you make that distinction between those that know Sass and those that know CSS. Once of the projects I'm currently involved in involves on-boarding designers to a Sassy codebase and I've really had to take a step back and look at it from their perspective and it's quite shockingly complex. It's easy to sell variables, but when you start explaining a mixin that calls a function that does a lookup on a map using a strange functional style to someone who is used to pure CSS you can hear the gears grinding to a halt.

That said, there has to be a way to leverage Sass to make setting properties across breakpoints cleaner. I'm currently experimenting with using a rhythm unit; single, double, half, quarter etc that can be dropped in as a value for padding or margin and is adjusted automatically across breakpoints from a centralised configuration. For example, by default, single might equate to rem(10), but at medium it opens out to rem(13). This is massively reducing the number of disparate and repeated values through a project and greatly encourages consistency.

because you can't reference another place in the map while it's still being defined.

This is indeed a real drag, but if you break the mega-map into multiple maps including a few base maps, this can be (largely) negated. It is also worth thinking about these maps as a point where configuration is stored as opposed to a place where configuration is defined. This ties into my comments earlier on theming.

I think this (configuration and it's half-brother setting properties across breakpoints) is currently an unsolved problem but I think there is enough flexibility in Sass that there is a good solution out there.

gakimball commented 9 years ago

@AshleyAitken I think some settings will definitely use maps, like breakpoints. It makes outputting, say, visibility classes here way easier, and adding new breakpoints would be super simple. Component settings are the bigger concern. Let's say three big benefits of having arrays are structure, looping, and variable access. For component settings, you only get the first benefit, not the other two. There would never be a need to write an @each loop for button settings, nor would accessing the properties dynamically ever be useful. So you mostly get the benefit of hierarchy, and the question is if that benefit is worth the overhead.

Undistraction commented 9 years ago

@AshleyAitken Where do you draw the line though between what should and shouldn't be configured on a breakpoint by breakpoint basis? Font-size, line-height, box-properties, offsets, sizes and even things like background image, font-smoothing are all props which often need to change at different breakpoints. The real advantage of using maps is that they allow relationships and nesting and more importantly, they don't rely on predefined keys / names. This is absolutely critical when you get into breakpoints which are arbitrary. The idea that there are 4 or 5 predefined breakpoints seems increasingly inadequate and therefore so does the idea that you can create variables to cope with all possibilities.

gakimball commented 9 years ago

@Undistraction I think I know what you're getting at when it comes to defining versus setting configuration. There's a question of usage, also.

We know our users are way into the settings file. A common criticism of front-end frameworks is that they're hard to override, so our settings file makes theming the framework components easier.

So setting variables is a common practice for our user. But accessing variables is probably a less common practice. There are common ones that people use a lot, like $primary-color or $column-gutter. It's mostly the more high-level settings that allow you to keep your designs aligned with the framework that people use a lot. But, for example, I doubt people often reference $topbar-dropdown-link-color in their custom CSS. They might set that variable to customize the framework, but they'll never reference it again.

DerFrenk commented 9 years ago

I'm surprised that nobody mentioned it earlier: IDE code completion. When I type $topbar-color in PhpStorm, it automatically shows a list of all available colors for the topbar. This list contains all available colors for the component like $topbar-bg-color, $topbar-link-color, $topbar-dropdown-link-color, etc.

I don't have to remember any of those variables, because they're autocompleted for me. I'm afraid that won't happen when using the foundation() function ;)

gakimball commented 9 years ago

@DerFrenk You make a good point, although we can't quite assume people are using an IDE with our library. But you're definitely right, since the accessor syntax is a function, an IDE can't intelligently make suggestions for you.

viztastic commented 9 years ago

@gakimball If I may, I am a classic example of a designer who uses Sass for its simplicity rather than its ability to provide architectural elegance.

I think you guys have done an amazing job with making your variables meaningful and accessible. I would certainly encourage the notion of prefixing with 'foundation' to avoid namespace collisions, but overall I am a big fan of simple variable lists and don't see any real value in maps beyond grouping tightly related variables together to facilitate simple looping.

IDE auto completion is also important to me.

My 2c... and very excited about Foundation Apps!

gakimball commented 9 years ago

@viztastic Since we had this initial discussion, we have found maps to be most useful when used for a very specific purpose. Being able to write loops to output certain classes is super nice. It's especially going to make editing and adding new breakpoints super easy.

The variable dump solution isn't ideal, but it's still going to be easiest for our users to wrap their heads around. As for namespacing, we haven't encountered many conflicts with other libraries, although it's possible that really generic mixins and functions can fight with each other.

zurbrandon commented 9 years ago

Great discussions on this, we took a lot of these ideas and stayed with an approach with less friction for the end user. Thanks for the killer discussion.