flex-layout icon indicating copy to clipboard operation
flex-layout copied to clipboard

Container Query / Relative query to an anchor

Open no-more opened this issue 6 years ago • 20 comments

Feature Request

What is the desired behavior?

It would be great to go further by implementing Container Query. ngx-responsive already do something similar for adding/removing an element based on media queries or Container Queries (responsive-window feature). Of course this might require a more complex syntax to be used, but it would be so powerfull when used with components which size varies not only according to screen size but also relative to a container size.

What is the use-case or motivation for the desired behavior?

Really responsive components that could be reused in multiple place with different size

Is there anything else we should know?

I'm quite surprise nobody asked that before. This would be a killer feature. Thanks

no-more avatar Dec 03 '18 15:12 no-more

@no-more I've read through the spec and the README for that package and I'm afraid I'm no closer to knowing what it is that you want. Could you provide a practical example of what this would look like as part of the Angular Layout API?

CaerusKaru avatar Dec 03 '18 15:12 CaerusKaru

Thanks for taking time to look at it. Maybe it wasn't the clearest example. Container queries might be included one day in css, but it's not for soon.

The objective is to create "media queries" for container, so instead of taking screen size as a reference to apply a specific rule, the idea is to apply the rule according to a parent container size.

An example, I have a reusable component which display a list, and I want to make it responsive by hiding some fields (or changing flex layout) according to the size of the component itself. This way I can easily use my component in a full page, in a sub component that occupies only a portion of the page or in a limited width popup. Same component, same screen size, but multiple layout according to the size of the containing element.

ngx-responsive already implemented something like this to show/hide some elements according to a container size. Here is their example:

<div [responsive-window]="'parent'">
     <p *responsive="{ sizes:{  window: 'parent', min:900, max:1400} }"></p>
</div>

Here it's define an anchor (named parent), and the element p is shown only if the parent size is between min and max. I think it should be possible to use breakpoint names with a little tweak.

So my request is to be able to do the same sort of responsivness with flex-layout directives.

no-more avatar Dec 03 '18 16:12 no-more

A syntax proposition :

<div fxContainer #parent>
     <div [fxLayouts]="{ container: parent, layouts: [{type: "row", warp: true, breakpoints: ["md", "gt-lg"]}, {type: "column", default: true}] }">...</div>
</div>

In this case the inner div layout would be by default column, and in case parent size is in md or gt-lg (random example) it would be row warp

This is just an example in order to try to show how it could be used. There might be a simpler syntax to achieve the same result

no-more avatar Dec 03 '18 17:12 no-more

Maybe an even easier way would be to use the component itself as reference

no-more avatar Dec 03 '18 17:12 no-more

Maybe it could be just much closer syntax to current and by default there would be a single fxContainer (a window). But when providing another container it would become the the size reference for its children, and could be provided with different breakpoints.

< div fxContainer="breakpoints[ ]" > < div fxLayout="column" fxLayout.gt-sm="row" ></ div > < /div >

olaf89 avatar Dec 03 '18 17:12 olaf89

@no-more just curious as to the reasoning behind using a container for breakpoint detection instead of the viewport as is currently used, can you please provide a real world example where this is used?

I do see a possible benefit of having a similar syntax but using the breakpoint detection that currently exists, for example:

<div [fxLayout]="[{ value: 'row wrap', breakpoints: ['md', 'gt-lg'] }, { value: 'column', default: true }]">
    <div fxFlex="5rem">…</div>
</div>

This would allow breakpoint information to be built up in code and then just passed to the property (e.g. <div [fxLayout]="myBreakpointConfig">...</div>)

charsleysa avatar Dec 04 '18 07:12 charsleysa

Hello,

Viewport is screen size, now let's say I have a contact list component. And I want to use it in several place. Firstly in a contact page list component that display the list almost in full screen. Then in a contact popup/modal witch max width is always 700px. So even if my screen is 1280px I want the component to be displayed only for 700px. Currently it's not possible with flex-layout as breakpoint are relative to viewport/screen size.

In this example I could have defined that if my container size is md or gt-lg then I want the subdiv to be row warp displayed, else column.

About the syntax the simplest will be the best.

I don't know if the feasible but something like this would be great :

<div fxContainer #parent> <!-- optional alias -->
	<div fxFlexLayout="column" fxFlexLayout.md="row warp" fxFlexLayout.gt-lg="row warp">
		<!-- div will flexed according to viewport -->
		...
	</div>
	<div fxContainer> <!-- if alias not specified it will be default for all children -->
		<div fxFlexLayout="column" fxFlexLayout.md="row warp" fxFlexLayout.gt-lg="row warp">
			<!-- div will flexed according to default one defined above -->
			...
		</div>
		<div fxFlexContainer="parent" fxFlexLayout="column" fxFlexLayout.md="row warp" fxFlexLayout.gt-lg="row warp" >
			<!-- div will flexed according to "parent" size defined above -->
			...
		</div>
	</div>
</div>

I think this is quite complete and could be applied for all flex-layout directives.

no-more avatar Dec 04 '18 07:12 no-more

And to go even further custom breakpoints could be defined locally because a reusable component might require different breakpoints that the application it's used in.

In this case breakpoints configuration could be injected locally, or could be passed as a parameter to the fxContainer:

<div fxContainer="customBreakpointConfig" #parent> <!-- optional alias -->
	<div fxFlexLayout="column" fxFlexLayout.md="row warp" fxFlexLayout.gt-lg="row warp">
		<!-- div will flexed according to viewport -->
		...
	</div>
	<div fxContainer="customBreakpointConfig"> <!-- if alias not specified it will be default for all children -->
		<div fxFlexLayout="column" fxFlexLayout.md="row warp" fxFlexLayout.gt-lg="row warp">
			<!-- div will flexed according to default one defined above -->
			...
		</div>
		<div fxFlexContainer="parent" fxFlexLayout="column" fxFlexLayout.md="row warp" fxFlexLayout.gt-lg="row warp" >
			<!-- div will flexed according to "parent" size defined above -->
			...
		</div>
	</div>
</div>

no-more avatar Dec 04 '18 07:12 no-more

Viewport is screen size, now let's say I have a contact list component. And I want to use it in several place. Firstly in a contact page list component that display the list almost in full screen. Then in a contact popup/modal witch max width is always 700px. So even if my screen is 1280px I want the component to be displayed only for 700px. Currently it's not possible with flex-layout as breakpoint are relative to viewport/screen size.

With that particular example the solution would be to have the contact list component always display at almost max width of container (like the first part of your example) and then for the popup restrict the popup container to 700px. This technically does not need to use the breakpoints.

Though I do see your use case for adapting the same component to display differently when used in a popup that has a smaller container size than the viewport.

In my opinion this would be a lower priority feature as there are currently different solutions available to work around these limitations. This library heavily uses and relies on the media query API, and there is not yet any support for performing media queries on elements.

One possible way to implement this would be to use ResizeObserver which would essentially give us the ability to perform media queries on elements though a bit of custom processing would be required to turn a media query string into JS code that can utilize element properties. It's currently only shipped in Chrome and Opera with Firefox under development and Edge under consideration. There are polyfills available but they use requestAnimationFrame() which can come with it's own limitations though it should be reliable enough in my opinion.

@CaerusKaru thoughts?

charsleysa avatar Dec 04 '18 08:12 charsleysa

I've been putting this off for days now trying to come up with some way to justify this. The inherent issue here is that this is a solved problem without an intermediate solution. As @charsleysa mentioned, the perfect solution to this is ResizeObserver. Unfortunately, it's not polyfillable, and will never be in IE11, which we still support.

The performance issues with the polyfills that are out there make them non-viable. If there's some way that I haven't considered to fold this in to the existing API without an enormous performance cost, I would be thrilled to add it to the library. Unfortunately I haven't been able to even think of a possible design so far.

I'll keep this issue open in case others have ideas. I'm sorry I don't have better news.

CaerusKaru avatar Dec 06 '18 04:12 CaerusKaru

@CaerusKaru according to caniuse.com the requestAnimationFrame() API is supported in IE11.

After some more investigation I think using the a simple ResizeObserver polyfill that utilizes requestAnimationFrame() is performant enough for this feature.

If the use of the observer is only kept to when this feature is used then it won't have an impact for existing users of the library and anyone wanting to use this feature will understand the potential performance impacts.

See this codepen for a really simple ResizeObserver polyfill that works in IE11 (it needs a little bit of work as I think it has a potential memory leak when creating / deleting many ResizeObserver objects).

charsleysa avatar Dec 06 '18 05:12 charsleysa

@charsleysa Excellent job researching this. I'm not crazy about requestAnimationFrame in general, especially because in that polyfill it's essentially on loop. I'm also not crazy about us supporting a polyfill in general.

I am willing to do this: we could design an API around ResizeObserver and have instructions on how to polyfill with a link to that source, or others, and have users make the end decision.

That just leaves the implementation and API. I'm also not a fan of the APIs suggested above because they all seem very, very verbose. The closest thing that I liked was @olaf89's fxContainer idea. Once there's an agreed-upon API, or a PR to review, I'd be happy to step in further.

CaerusKaru avatar Dec 06 '18 05:12 CaerusKaru

I'm happy to see this is interesting other people.

I'm aware about the resizeObserver issue, after looking in ngx-responsive I can understand how they go around it. It's far from a perfect solution, but in my case, and maybe for a lots of other people, it's doing the job.

So what they did, instead of observing directly the component size, they register to window resize event, and then check if the component size have changed. I think this works fine for 75% of users as most of the time a component will resize due to a screen resize. And at least it cover actual flex-layout behavior with an extra bonus (according to me). I have submitted a PR that allow to manually trigger a refresh (for example button click to toggle split view, or after a component refresh). I think with this extra feature it covers over 95% use cases.

It could be a fallback solution that rely on resizeObserver when available, and switch to then otherwise.

I'm talking for myself, but that would perfectly fill my needs.

About the syntax if I transpose what I've done on ngx-responsive, it could be something like this :

<div fxLayoutContainer #myParentContainer>
    <div fxLayout="row" fxLayout.lt-sm="column" [fxRelativeTo]="myParentContainer">
        ....
    </div>
</div>

This "just" require to add an other input to existing directives. If this input is an angular component then on event (from one of the observer defined above) use this size for computation instead of screen size. The only point I'm not sure is it possible to define same input on a element for multiple directive, or do we have to define one specific named input per directive?

What do you think about it?

no-more avatar Dec 06 '18 10:12 no-more

With ngx-responsive and their use of the resize event, from my research I have found two reasons to not use it:

  1. it doesn't fire when the elements resize, only the viewport, so if you're wanting to style components based on container size changes that occur without the viewport changing size then it won't work
  2. the resize events can be fired as many times as the browser wants, and some browsers burst the resize event faster than the browser draws so you'll potentially end up doing a lot of computations without any benefit (and potentially cause high latency drawing if you are doing too much during each fire of the event)

In regards to the syntax, I like what you've proposed though it could possibly be even simpler:

<div #myParentContainer>
    <div fxLayout="row" fxLayout.lt-sm="column" [fxBreakpointContainer]="myParentContainer">
        ....
    </div>
</div>

In regards of defining the same input, instead of requiring every directive to have an input you can use Angular's Dependency Injection to inject the instance of fxBreakpointContainer (or whatever it ends up being called) with an Optional attribute. That way we can determine whether to use a media query service or a resize observer service (implementation detail still to be figured out).

charsleysa avatar Dec 06 '18 10:12 charsleysa

I like your proposal, its very easy to understand.

About the resize event, isn't it already used by flex-layout ? I haven't checked but if flex-layout doesn't use it and use another source of even then we can use the same source. About refresh rate, ngx-responsive introduced a debounce time parameter (injectable in the configuration). It would be great to have the same with flex-layout (independently of this feature request).

About your first argument, that's true, this solution does not respond to 100% of use case. But I really think it works for 75% of most users' needs. And when available it's still possible to switch to resizeObserver. And adding a manual refresh method (https://github.com/ManuCutillas/ngx-responsive/pull/124) to force the update on some events could make the ratio up to 90-95% (that's my belief)

no-more avatar Dec 06 '18 11:12 no-more

This library uses the media query API (not the resize event) which only triggers when the media query match state changes so a lot more performant.

This also means that it can't be used for element resize because the window could be resized multiple times without the media query match state changing.

charsleysa avatar Dec 06 '18 11:12 charsleysa

Thanks for the enlightenment, I didn't know this API

no-more avatar Dec 06 '18 12:12 no-more

So here's where I think I've landed on this. The API would be TemplateRef-based and look like this:

<div #parent>
  <div fxLayout.relative="parent" fxLayout.xs="column" fxLayout="row">
	...
  </div>
</div>

This would allow a terse declaration of the container while still providing us the necessary information to track the element.

We would use ResizeObserver under the hood, meaning anyone wishing to use .relative (which would now be a reserved input breakpoint key) would have to polyfill it.

The final concern is that this wouldn't work on SSR since Angular's DOM provider on the server has no conception of window size (or element size). That would make the elements seem different (but hopefully not radically different) on the server. This would be noted in the docs but is unfortunately a sticking point.

CaerusKaru avatar Jan 07 '19 14:01 CaerusKaru

Hello, some news about this awesome functionality ?

stephanebrun avatar Nov 23 '20 11:11 stephanebrun

Hi, any news about this feature?

agallardol avatar Dec 23 '20 15:12 agallardol