Transitions between different knockout components

2019-09-08 15:43发布

问题:

I'm trying to apply CSS transitions as I switch between knockout components but I'm not having much joy in achieving this. Essentially I want to have a div with a fixed width, but the internal content of which will change. As it does this I want to be able to transition the re-sizing of the element.

ko.components.register("big", {
    viewModel: function (vm) {
        this.items = vm.value.items;
    },
    template: '<div class="big box" data-bind="foreach: items"><p class="item" data-bind="text: name"></p></div>'
});

ko.components.register("small", {
    viewModel: function (vm) {        
        this.items = vm.value.items;
    },
    template: '<div class="small box" data-bind="foreach: items"><span class="item" data-bind="text: name"></span></div>'
});






var vm = {};
vm.componentName = ko.observable("small");
vm.items = ko.observableArray([{ name: "A" }, { name: "B" }, { name: "C" }]);
ko.applyBindings(vm);

setInterval(function() {
    if(vm.componentName() === "small") { vm.componentName("big"); }
    else { vm.componentName("small"); }
}, 3000);
.box {
    width: 200px;
    -webkit-transition: height 2s;
    transition: height 2s;
    -webkit-transition: width 2s;
    transition: width 2s;
}
.big {
    border: thin solid black;
}
.small {
    border: thin solid black;
    padding: 10px 10px 10px 10px;
}
.item {
    padding-left: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="component: { name: componentName, params: { value: $data } }">
</div>

So I've asked something vaguely related Why doesn't CSS transition get applied? where I learnt that the DOM is being re-constructed for the new value (in this case new template) and hence CSS transitions don't apply.

The solution there was simple (ensure you bind to the same thing repeatedly). But for components, I really don't want to combine two templates together just so I can get transitions. Is there another way I can achieve this?

回答1:

You're actually switching the component meaning the DOM is being re-constructed, so I don't see a way to animate using CSS.

What you can do is build your own binding handler which does the animating for you using Javascript:

ko.bindingHandlers.animatingComponent = {
    init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
        var value = valueAccessor();
        var componentName = value.name;
        // create a new observable so we can delay the moment ko's component 
        // binding builds the new component
        var actualComponentName = ko.observable(componentName());
        componentName.subscribe(function(newComponent) {
            $(element).hide(500, function() {
                actualComponentName(newComponent);
                $(element).show(500);
           });
        });

        ko.bindingHandlers.component.init(element, function() { 
            return { name: actualComponentName, params: value.params}; 
        }, allBindings, viewModel, bindingContext);
    }
};

ko.components.register("big", {
    viewModel: function (vm) {
        this.items = vm.value.items;
    },
    template: '<div class="big box" data-bind="foreach: items"><p class="item" data-bind="text: name"></p></div>'
});

ko.components.register("small", {
    viewModel: function (vm) {        
        this.items = vm.value.items;
    },
    template: '<div class="small box" data-bind="foreach: items"><span class="item" data-bind="text: name"></span></div>'
});


var vm = {};
vm.componentName = ko.observable("small");
vm.items = ko.observableArray([{ name: "A" }, { name: "B" }, { name: "C" }]);
ko.applyBindings(vm);

setInterval(function() {
    if(vm.componentName() === "small") { vm.componentName("big"); }
    else { vm.componentName("small"); }
}, 3000);
.box {
    width: 200px;
}
.big {
    border: thin solid black;
}
.small {
    border: thin solid black;
    padding: 10px 10px 10px 10px;
}
.item {
    padding-left: 10px;
}
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="animatingComponent: { name: componentName, params: { value: $data } }">
</div>



回答2:

It only took most of the day, but I got the transition to work. I have a variable that remembers the last display size (which is where it will start when the component changes). When the component changes, I set visibility to hidden and sizes to default so I can get the intended size of the div. Then I size it back to the last display size and make it visible, then size it to the new size, and the transition happens.

Note that I also had to change the CSS a bit.

ko.components.register("big", {
viewModel: function (vm) {
    this.items = vm.value.items;
},
template: '<div class="big box" data-bind="style:$root.boxSize, foreach: items, sizeGet:$root.boxSize"><p class="item" data-bind="text: name"></p></div>'
});

ko.components.register("small", {
viewModel: function (vm) {
    this.items = vm.value.items;
},
template: '<div class="small box" data-bind="style:$root.boxSize, foreach: items, sizeGet:$root.boxSize"><span class="item" data-bind="text: name"></span></div>'
});


var unclipped;
ko.bindingHandlers.sizeGet = {
init: function (element, valueAccessor) {
    var sizer = valueAccessor();
    sizer({
        height: '',
        width: '',
        visibility: 'hidden'
    });
    var nextUnclipped = {
        height: element.scrollHeight + 'px',
        width: element.scrollWidth + 'px',
        visibility: 'visible'
    };

    if (unclipped) sizer(unclipped);
    unclipped = nextUnclipped;
    setTimeout(function () {
        sizer(unclipped);
    }, 0);
}
};

var vm = (function () {
var activeComponent = ko.observable('small'),
    defaultSize = {
        width: '',
        height: ''
    },
    boxSize = ko.observable(defaultSize);

return {
    componentName: activeComponent,
    boxSize: boxSize,
    items: ko.observableArray([{
        name: "A"
    }, {
        name: "Big"
    }, {
        name: "Cat"
    }, {
        name: "Dropping"
    }])
};
}());
ko.applyBindings(vm);

var i = setInterval(function () {
if (vm.componentName() === "small") {
    vm.componentName("big");
} else {
    vm.componentName("small");
}
}, 3000);

setTimeout(clearInterval.bind(null, i), 45000);
.box {
    width:200px;
    -webkit-transition: height 2s, width 2s;
    transition: height 2s, width 2s;
    overflow:hidden;
}
.big {
    border: thin solid black;
}
.small {
    border: thin solid black;
    padding: 10px 10px 10px 10px;
}
.item {
    padding-left: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="component: { name: componentName, params: { value: $data } }">
</div>