Reputation: 101
I have a fixed height div (body) containing two children, the header and the content. The header's height is changing on click of a button, and the content's height should be auto adjusting to fill the rest of the body. Now the problem is, the new header's height is calculated after the header and content divs got rendered, so the content div's height won't be updated upon the button click. Here's the shortened code:
return m('.body', {
style: {
height: '312px'
}
}, [
m('.header', /* header contents */),
m('.content', {
style: {
height: (312 - this._viewModel._headerHeight()) + 'px'
}
}, /* some contents */)
])
The headerHeight function calculates the header's height and applies changes to it. However the new height is calculated after it's rendered thus won't be applied immediately to the calculation of content's height - there's always a lag.
Any idea to fix it?
Upvotes: 2
Views: 521
Reputation: 16456
This is a regular problem when dealing with dynamic DOM layouts in which some writable DOM properties are derived from other readable DOM properties. This is especially tough to reason about in declarative virtual DOM idioms like Mithril because they're based on the premise that every view function should be self-complete snapshots of UI state — which in this case isn't possible.
You have 3 options: you can either break out of the virtual DOM idiom to achieve this functionality by directly manipulating the DOM outside of Mithril's view, or you can model your component to operate on a '2 pass draw', whereby each potential change to the header element results in 1 draw to update the header and a second draw to update the content accordingly. Alternatively, you might be able to get away with a pure CSS solution.
Because you only need to update one property, you're almost certainly better off going for the first option. By using the config
function, you can write custom functionality that executes after the view on every draw.
return m('.body', {
style: {
height: '312px'
},
config : function( el ){
el.lastChild.style.height = ( 312 - el.firstChild.offsetHeight ) + 'px'
}
}, [
m('.header', /* header contents */),
m('.content', /* some contents */)
])
The second option is more idiomatic in terms of virtual DOM philosophy because it avoids direct DOM manipulation and keeps all stateful data in a model read and applied by the view. This approach becomes more useful when you have a multitude of dynamic DOM-related properties, because you can inspect the entire view model as the view is rendered — but it's also a lot more complicated and inefficient, especially for your scenario:
controller : function(){
this.headerHeight = 0
},
view : function( ctrl ){
return m('.body', {
style: {
height: '312px'
}
}, [
m('.header', {
config : function( el ){
if( el.offsetHeight != ctrl.headerHeight ){
ctrl.headerHeight = el.offsetHeight
window.requestAnimationFrame( m.redraw )
}
}, /* header contents */),
m('.content', {
style : {
height : ( 312 - ctrl.headerHeight ) + 'px'
}
}, /* some contents */)
])
}
A third option — depending on which browsers you need to support — would be to use the CSS flexbox module.
return m('.body', {
style: {
height: '312px',
display: 'flex',
flexDirection: 'column'
}
}, [
m('.header', {
style : {
flexGrow: 1,
flexShrink: 0
}
}, /* header contents */),
m('.content', {
style : {
flexGrow: 0,
flexShrink: 1
}
}, /* some contents */)
])
This way, you can simply state that the container is a flexbox, that the header should grow to fit its content and never shrink, and that the contents should shrink but never grow.
Upvotes: 2