GithubHelp home page GithubHelp logo

chadian / ember-fill-up Goto Github PK

View Code? Open in Web Editor NEW
19.0 4.0 2.0 2.98 MB

Friendly container queries for Ember

Home Page: https://chadian.github.io/ember-fill-up

License: MIT License

JavaScript 73.39% HTML 3.13% CSS 11.10% Handlebars 12.38%
element-query element-queries ember-addon responsive-web-design component-architecture ember container-query container-queries responsive-components

ember-fill-up's Introduction

⛽️ ember-fill-up Build Status

⚠ No Longer Maintained

This project is no longer maintained. This addon was a proof-of-concept for my EmberFest talk previewing a user-friendly API for container/element queries and showcasing a few different strategies for respsonive components. At the time ember community was still discovering how best to support element resize events with element modifiers. I am happy to say that the base primitives have matured and an implementation that best illustrates these ideas continued in a newer addon by Isaac Lee with his project ember-container-query. I highly recommend everyone use it!


This addon gives you tools to help you create responsive components that are easier to develop, test, and use.

Instead of using media queries ember-fill-up focuses on using container queries. Container queries, also known as element queries, end up being more powerful especially when building out responsive components. You can get the general idea of Container Queries from this article, however it should be noted that this addon tries to achieve the same result in an Ember Way™️ with components.

This addon does not aim at providing support for the custom css element queries syntax, however with this addon you should be able to achieve many of the same solutions in designing your responsive components.

ember-fill-up detects changes on elements by using a ResizeObserver. If your browser doesn't support ResizeObserver then you may need to install a polyfill.

Check out the motivations and ideas behind ember-fill-up in my talk at EmberFest 2019.

Demo

Check out the demo app (currently requires a browser that supports ResizeObserver)

Compatibility

  • Ember.js v2.16 or above
  • Ember CLI v2.13 or above
  • Node.js v10 or above

Installation

ember install ember-fill-up

If your browser does not support ResizeObserver natively then you will need to install a polyfill. There are a few options that you can try (1, 2).

Polyfill Example

For this example we can set up the que-etc/resize-observer-polyfill polyfill with our dummy app.

First we install the polyfill dependencies:

npm install resize-observer-polyfill

Then we create an intializer:

ember g initializer setup-resize-detector-polyfill

Inside the initializer file app/initializers/setup-resize-detector-polyfill.js we add the ResizeObserver polyfill when it doesn't exist:

import ResizeObserver from 'resize-observer-polyfill';

export function initialize() {
  if (!window.ResizeObserver) {
    // eslint-disable-next-line no-console
    console.info(
      'initializer:setup-resize-detector-polyfill: ResizeObserver not found. Polyfilling...'
    );
    window.ResizeObserver = ResizeObserver;
  }
}

export default {
  before: 'register-resize-observer-detector',
  initialize
};

Note: To be able to import from npm packages like this you maybe need to install ember-auto-import in your app.

Your app should be all set now for older browsers that don't support ResizeObserver. As an example, the dummy app of this addon has been setup with this initializer.

Usage

Fill Up primatives

  1. {{fill-up}} element modifier The element modifier can be placed on any element and a resize detector will be installed on it. Anytime a change is detected on that element the onChange callback is called.
<div {{fill-up onChange=this.changeHandler}}>
</div>

The changeHandler in this example will receive a single argument which is the element on which the change occured.

  1. <FillUp /> component The <FillUp /> component is using the element modifier behind the scenes, and provides additional niceties making it easier to manage detected changes.

One fundamental difference is that the component between the component and the element modifier is that the component has a root element where all content from the component's block is put. It's changes from this root element that are being tracked.

Using the <FillUp /> component

The <FillUp /> component is pretty useful and abstracts away the element modifier while its api provides several handy responsive features.

breakpoints

The breakpoints can be passed in via definitions defined by template helpers in the template or by functional definitions defined in javascript. It should be noted that all breakpoint values here are using numbers represented by pixel values.

Breakpoint definitions
  • gt(value [, options])
    • @param {number} value - The value to test whether or not the dimension is greater than
  • gte(value [, options])
    • @param {number} value - The value to test whether or not the dimension is greater than or equal to
  • lt(value [, options])
    • @param {number} value - The value to test whether or not the dimension is less than
  • lte(value [, options])
    • @param {number} value - The value to test whether or not the dimension is less than or equal to
  • eq(value [, options])
    • @param {number} value - The value to test whether or not the dimension is equal to
  • between(inclusiveLowerBound, exclusiveUpperBound [, options])
    • @param {number} inclusiveLowerBound - The (inclusive) lowerbound to compare if the value is greater than or equal to.
    • @param {number} exclusiveUpperBound - The (exclusive) upperbound to compare if the value is less than.

All definitions accept an optional options argument. For examples on how to specify these for the template helpers or functional javascript breakpoint definitions check out the example tall breakpoint in the sections below. options

  • @param {Object} [options] - (optional) Options that can be passed to provide additional context to the defintion, currently only used for specifying the dimension.
  • @param {"width"|"height"} [options.dimension="width"] - (optional) Passing in a key of dimension with a value of width or height will specify which dimension the breakpoint definition is for. By default, if no options are passed in, the "width" dimension will be used.
Template helper breakpoint definitions (defined in the template)
  <FillUp
    @breakpoints={{hash
      small=(fill-up-lte 500)
      large=(fill-up-gt 500)
      tall=(fill-up-gt 700 dimension="height")
    }}
  ></FillUp>
Functional breakpoint definitions (defined in javascript):
import { lte, gt } from 'ember-fill-up/definitions';

// ...
// on the component definition:

    breakpoints: {
      small: lte(400),
      large: gt(400),
      tall: gt(700, { dimension: 'height' })
    }

With the definition on your component on the breakpoints property they can then be passed into the @breakpoints argument on the <FillUp /> component.

  <FillUp @breakpoints={{this.breakpoints}}>

@breakpoints argument

Any breakpoint definitions passed in to the @breakpoints argument of the <FillUp /> component will be turned into attribute labels on the component's root div element when those breakpoints are active.

For example:

  <FillUp
    @breakpoints={{hash
      small=(fill-up-lte 500)
    }}
  ></FillUp>

would end up with the following root element, only when the small breakpoint is active:

<div [fill-up-small]></div>

You can also override the fill-up prefix seen here in the attribute fill-up-small, by specifying an @attributePrefix argument on the component.

For example:

  <FillUp
    @attributePrefix="bp-"
    @breakpoints={{hash
      small=(fill-up-lte 500)
    }}
  ></FillUp>

would end up with the attribute prefix bp-small:

<div [bp-small]></div>

onChange handler

The changeHandler passed to onChange on the component will be called whenever a size change is detected on component's root element. The onChange on the <FillUp> component is different than the one on the element modifier, it receives an object with additional useful properties.

Example:

<FillUp onChange={{changeHandler}}></FillUp>

onChange(change)

  • @param {Object} change - The change object containing useful items relevant to the change
  • @param {string} change.element - The element from which a size change was detected
  • @param {string} change.width - The clientWidth of the changed element
  • @param {string} change.height - The clientHeight of the changed element
  • @param {Object.<string, boolean>} change.breakpoints - If breakpoints were passed in, this would represent a hash of breakpoint labels assigned to a boolean representing whether or not the breakpoint is active for the current change.

FillUp Block Param

The component also yields a useful block param, in this example, denoted by F: <FillUp as |F|></FillUp>. This block param provides useful information of the most recent change.

|F| block param

  • @param {Object} F - The last change object containing useful items relevant to the change
  • @param {string} F.element - The element from which a size change was detected
  • @param {string} F.width - The clientWidth of the changed element
  • @param {string} F.height - The cleintHeight of the changed element
  • @param {Object.<string, boolean>} F.breakpoints - If breakpoints were passed in, this would represent a hash of breakpoint labels assigned to a boolean representing whether or not the breakpoint is active for the current change.

Passing attributes

Any attributes specified on the <FillUp /> component will be "splatted" on the component's root div.

For example:

<FillUp class="hello-friends"></FillUp>

Results in the component's root div element receiving the class:

<div class="ember-fill-up hello-friends"></div>

This applies for other attributes that you might want to set on the root element.

Note: This only applies to the angle-bracket invokation of the component, see below for the limtiations related to the cury-bracket usage.

Curly-Bracket Usage

The curly bracket invokation of the fill-up component will also work with the examples in this documentation with the one exception of being able to "splat" attributes. In this case the only attribute that can be set are classes via the classNames argument (which should only be used for the curly-bracket invokation of the fill-up component)

{{#fill-up classNames="hello-friends"}}
{{/fill-up}}

Results with the root div element receiving the class:

<div class="ember-fill-up hello-friends"></div>

Responsive Component Strategies

As a way of getting started you could consider one of following three strategies for making a responsive component.

  1. CSS Breakpoint Selectors (see the example in the dummy demo app)

By passing in a class and using the active attributes available on the root element of the <FillUp /> component you can use CSS selectors to style things appropriately.

In this example we've passed in a class of my-component and a single breakpoint for when a breakpoint is greater than 500 pixels. In our css below, by default, there is a font-size of 15px for this component using the my-component class. When the attribute [fill-up-large] is applied when the large breakpoint is active the .my-component[fill-up-large] selector will apply, changing the font-size to 50px.

  <FillUp
    class="my-component"
    @breakpoints={{hash
      large=(fill-up-gt 500)
    }}
  ></FillUp>
.my-component {
  font-size: 15px;
}

.my-component[fill-up-large] {
  font-size: 50px;
}
  1. Responsive Sprinkles (see the example in the dummy demo app)

The idea behind this technique is to use the yielded block param and use the necessary breakpoint information conditionally where applicable.

  <FillUp
    @breakpoints={{hash
      small=(fill-up-lte 500)
      large=(fill-up-gt 500)
    as |F|
    }}
  >
    {{#if F.breakpoints.small}}
      Look this will only be rendered when
      the small breakpoint is active.
    {{/if}}

    This will always be rendered!

    {{#if F.breakpoint.large}}
      This is shown when we have an active large breakpoint!
    {{/if}}

  </FillUp>
  1. Component Swap (see the example in the dummy demo app)

In the case you have components that need to look radically different given a breakpoint it might be easier to use child components and swap between them.

Here we would have a main <Greeting /> component, with three child components:

  • <Greeting::Small />
  • <Greeting::Medium />
  • <Greeting::Large />

When the <Greeting /> component is rendered, depending on which breakpoint is active, it will use the corresponding child component. It's important in this case to try and create symmetry between the component arguments and the data available to each of children components. In this case our parent <Greeting /> receives a @model argument that we're passing along to each of the chldren components.

greeting.hbs:

<FillUp
  @breakpoints={{hash
    small=(fill-up-between 0 800)
    medium=(fill-up-between 800 1200)
    large=(fill-up-gte 1200)
  }}
as |F|>
  {{#if F.breakpoints.large}}
    <Greeting::Large model={{@model}}/>
  {{else if F.breakpoints.medium}}
    <Greeting::Medium model={{@model}}/>
  {{else}}
    <Greeting::Small model={{@model}}/>
  {{/if}}
</FillUp>

Contributing

See the Contributing guide for details.

Thanks!

I want to thanks others who have worked on the concept of container and element queries before. Their work has made for invaluable reference in exploring the idea and current options.

* Lucas Wiener, Tomas Ekholm, and Philipp Haller also authored an excellent paper summarizing the differences in detecting changes in element sizes. I highly encourage reading it over.

License

This project is licensed under the MIT License.

ember-fill-up's People

Contributors

chadian avatar ctjhoa avatar dependabot[bot] avatar ember-tomster avatar lolmaus avatar turbo87 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

turbo87 lolmaus

ember-fill-up's Issues

CSS-first development experience

I've noticed a difference between the approaches of ember-fill-up and my old ember-element-query.


In ember-fill-up you need to define breakpoint values in px on HBS or JS level:

  <FillUp
    @breakpoints={{hash
      small=(fill-up-lte 500)
      large=(fill-up-gt 500)
    }}
  ></FillUp>
    breakpoints: {
      small: lte(400),
      large: gt(400),
    }

You then use semantic names for breakpoint ranges in CSS:

.my-element[fill-up-small] {}
.my-element[fill-up-large] {}

This works, but what I don't like about this approach is that presentational concerns leak into HBS/JS realm where they don't belong.

Note that HBS/JS not only has to know breakpoint px values, but also be aware exactly which breakpoint ranges the CSS is gonna utilize.


In ember-element-query you were able to use px breakpoint values directly on CSS level:

.my-element[data-eq-to~=400px] {}
.my-element[data-eq-from~=401px] {}

Or with Sass:

.my-element {
  $bp: 400px;
  @include eq-from($bp) {};
  @include eq-to($bp) {};
}

The benefits are that you don't need to come up with semantic names for breakpoints, keep them in sync between CSS and HBS/JS; and the styling concerns do not leak into HBS/JS.

The addon would parse resulting CSS for data-eq-* selectors at build time and expose them to the app as an importable JS module as a map of HTML classes to breakpoints. Element query components would then look up their breakpoints in the mapping and apply them automatically.

The approach I used to make that possible is quite questionable. It makes builds a bit slower and requires the mapping to be read with a separate HTTP request (because I failed to find a way to include it into the build -- when compiled CSS becomes available for parsing, JS is also compiled already).

Thus, I do not want to see it in any of my current projects. It felt quite appropriate in Ember Classic era, but it violates Octane and Embroider spirit.

But I find the goal to be quite noble and I wonder if there are Octane/Embroider-friendly ways of achieving it.

@chadian Please tell if you recognize the problem and if yes, share your thoughts.

Consider swapping resize detection library

This project offers a list of different resize detection methods to consider. One of these solutions, especially one that uses ResizeObserver with a MutationObserver fallback, may be more performant and more future-proof. After choosing the method(s) it's a matter of choosing the library that best suits the detection method.

Add `fill-up` element modifier

Most of the project so far has been based on the idea of measuring the root element of a component. As components move to inner html semantics this root element will no longer exist.
Element modifiers could likely support a similar design of what this addon does.

This will also clear up any issues where maybe a tagless component is used today. An element modifier probably has better ergonomics for the use cases that people would have in wanting to measure a component, too.

Resources:

Make FastBoot compatible

Support FastBoot so that this addon:

  • Doesn't break when rendering in a fastboot
  • Allows some fastboot fallback so users can choose what should be rendered

Provide a DSL for specifying breakpoints

Allow a way of providing breakpoints through a DSL to break down the breakpoints into a matched class or attribute. The design should hold up for computed properties and helpers in templates.

As a first pass maybe something like:

Definitions

breakpoints (would accept definitions from):

  • from
  • to
  • between
  • at
  • stack
// in a component definition
attrBindings: ['data-breakpoint'],
	['data-breakpoint']: breakpoints(
		to("medium", 200),
		stack("large", 500),
		stack("extra-large", 700)
	)
{{!-- in hbs --}}
{{#fill-up (
  fill-up-breakpoints(
    fill-up-to("small" 499)
    fill-up-between("medium" 500 700)
    fill-up-from("large" 701)
  )
)}}
  content here
{{/fill-up}}

Allow "resize strategy" to be set on the resize detector

Currently as a proof-of-concept this addon is using the default resize strategy provided by element-resize-detector. For different reasons it may be desirable to specify this and therefore should be a way to pass in the strategy preference.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.