Slider rating component

hi.

Im trying to implement slider style rating component with fuse.

I put 5 empty (grey) stars horizontally and make slider to fill it yellow.

but I don’t have any clue with fuse.

is it possible U think? please give me a hint if anyone implemented. thanks.

Hi may_P!

It’s definitely possible. And although it might seem trivial at first, making a truly reusable “rating component” turns out to be a quite involved task.

I took a stab at it and came up with a rather decent solution that I hope you’ll like. Make sure to grab a white star.png image from material icons pack and put it in the project root first.

Here’s the code:

<App Background="#fff">

    <!-- this StackPanel here is the reusable component. you should put it in a separate UX file. -->
    <StackPanel ux:Class="RatingComponent" Height="40" Orientation="Horizontal">
        <int ux:Property="Rating" />
        <int ux:Property="Stars" />
        <JavaScript>
            var Observable = require("FuseJS/Observable");

            var total = this.Stars;

            // the .mapTwoWay() is the really tricky part that binds to the outer Observable passed in and updates it as necessary
            // it might work with .innerTwoWay(), but I did not test
            var rating = this.Rating.mapTwoWay(function(v) {
                return v;
            }, function(v, sv) {
                return v;
            });

            var stars = [];
            for (var i = 0; i < total.value; i++) {
                stars.push(new Star(i));
            }

            function Star(id) {
                this.id = id;
                this.isActive = Observable(false);
            }

            function selectStar(args) {
                rating.value = args.data.id + 1;
            }

            rating.onValueChanged(module, function(x) {
                stars.forEach(function(s) {
                    if (s.id < x) {
                        s.isActive.value = true;
                    } else {
                        s.isActive.value = false;
                    }
                });
            });

            module.exports = {
                stars: stars,
                selectStar: selectStar
            };
        </JavaScript>
        <Each Items="{stars}">
            <Panel HitTestMode="LocalBounds">
                <Clicked>
                    <Callback Handler="{selectStar}" />
                </Clicked>
                <WhileTrue Value="{isActive}">
                    <Change theStar.Color="#FFC107" Duration="0.16" />
                </WhileTrue>
                <Image ux:Name="theStar" File="star.png" Color="#9E9E9E" />
            </Panel>
        </Each>
    </StackPanel>

    <!-- this here is the "main" app that makes use of the component -->
    <JavaScript>
        var Observable = require("FuseJS/Observable");
        var rating = Observable(2);

        rating.onValueChanged(module, function(x) {
            console.log("rating changed to: " + x);
        });

        module.exports = {
            rating: rating
        };
    </JavaScript>
    <RatingComponent Stars="5" Rating="{rating}" Alignment="Center" />
</App>

Thanks Uldis!

I didn’t expect get respond that fast.
I didn’t get understand ur code yet but will try today.

again thanks a lot man!

your code is works very well thanks Uldis and I need that moves in other screen.
I’ll use your code in there if you allow that.

I was wanted this action but I didn’t explained enough in my question.

is it possible in fuse?

Yes, it is. With a RangeControl: https://www.fusetools.com/docs/fuse/controls/rangecontrol

Well this was a fun exercise :slight_smile: Now you have two components.

Remember to first get the icons (search for ‘star’): https://material.io/icons/

<App Background="#fff">

    <!-- this StackPanel here is the first reusable component. you should put it in a separate UX file. -->
    <StackPanel ux:Class="RatingComponent" Height="40" Orientation="Horizontal">
        <int ux:Property="Rating" />
        <int ux:Property="Stars" />
        <JavaScript>
            var Observable = require("FuseJS/Observable");

            var total = this.Stars;

            // the .mapTwoWay() is the really tricky part that binds to the outer Observable passed in and updates it as necessary
            var rating = this.Rating.mapTwoWay(function(v) {
                return v;
            }, function(v, sv) {
                return v;
            });

            var stars = [];
            for (var i = 0; i < total.value; i++) {
                stars.push(new Star(i));
            }

            function Star(id) {
                this.id = id;
                this.isActive = Observable(false);
            }

            function selectStar(args) {
                rating.value = args.data.id + 1;
            }

            rating.onValueChanged(module, function(x) {
                stars.forEach(function(s) {
                    if (s.id < x) {
                        s.isActive.value = true;
                    } else {
                        s.isActive.value = false;
                    }
                });
            });

            module.exports = {
                stars: stars,
                selectStar: selectStar
            };
        </JavaScript>
        <Each Items="{stars}">
            <Panel HitTestMode="LocalBounds">
                <Clicked>
                    <Callback Handler="{selectStar}" />
                </Clicked>
                <WhileTrue Value="{isActive}">
                    <Change theStar.Color="#FFC107" Duration="0.16" />
                </WhileTrue>
                <Image ux:Name="theStar" File="star.png" Color="#9E9E9E" />
            </Panel>
        </Each>
    </StackPanel>

    <!-- this StackPanel here is the second reusable component. you should put it in another separate UX file. -->
    <Panel ux:Class="SlidingRatingComponent" Height="40">
        <float ux:Property="Rating" />
        <int ux:Property="Stars" />
        <JavaScript>
            var Observable = require("FuseJS/Observable");
            var progress = Observable(0);
            var currentRating = Observable(0);

            var total = this.Stars;
            // the .mapTwoWay() again binds to the outer Observable passed in and updates it as necessary
            var rating = this.Rating.mapTwoWay(function(v) {
                progress.value = v * 20;
                return v;
            }, function(v, sv) {
                return v;
            });

            var stars = [];
            for (var i = 0; i < total.value; i++) {
                stars.push(new Star(i));
            }

            function Star(id) {
                this.id = id;
                this.isActive = Observable(false);
                this.state = Observable("empty");
            }

            progress.onValueChanged(module, function(x) {
                var newVal = Math.round((x * 5) / 50) / 2;
                // this here is necessary so that the currentRating only changes its value in steps
                if (currentRating.value != newVal) currentRating.value = newVal;
            });

            currentRating.onValueChanged(module, function(x) {
                rating.value = x;
            });

            rating.onValueChanged(module, function(x) {
                var floored = Math.floor(x);
                var paintHalf = (x - floored) > 0;
                stars.forEach(function(s) {
                    if (s.id == (floored - 1) && !paintHalf) {
                        s.state.value = "full";
                        s.isActive.value = true;
                    } else if (s.id == floored && paintHalf) {
                        s.state.value = "half";
                        s.isActive.value = true;
                    } else if (s.id < floored) {
                        s.state.value = "full";
                        s.isActive.value = false;
                    } else {
                        s.state.value = "empty";
                        s.isActive.value = false;
                    }
                });
            });

            module.exports = {
                progress: progress,
                stars: stars
            };
        </JavaScript>
        <RangeControl Value="{progress}" UserStep="10" HitTestMode="LocalBounds">
            <LinearRangeBehavior />
        </RangeControl>
        <StackPanel Orientation="Horizontal">
            <Each Items="{stars}">
                <Panel>
                    <Match String="{state}">
                        <Case String="empty">
                            <Image File="stars/star-empty.png" Color="#FFC107">
                                <WhileFalse Value="{isActive}">
                                    <Scaling Factor="0.86" />
                                </WhileFalse>
                            </Image>
                        </Case>
                        <Case String="half">
                            <Image File="stars/star-half.png" Color="#FFC107">
                                <WhileFalse Value="{isActive}">
                                    <Scaling Factor="0.86" />
                                </WhileFalse>
                            </Image>
                        </Case>
                        <Case String="full">
                            <Image File="stars/star-full.png" Color="#FFC107">
                                <WhileFalse Value="{isActive}">
                                    <Scaling Factor="0.86" />
                                </WhileFalse>
                            </Image>
                        </Case>
                    </Match>
                </Panel>
            </Each>
        </StackPanel>
    </Panel>

    <!-- this here is the "main" app that makes use of the components -->
    <JavaScript>
        var Observable = require("FuseJS/Observable");
        var staticRating = Observable(2);
        var slidingRating = Observable(3);

        staticRating.onValueChanged(module, function(x) {
            console.log("staticRating changed to: " + x);
        });
        slidingRating.onValueChanged(module, function(x) {
            console.log("slidingRating changed to: " + x);
        });

        module.exports = {
            staticRating: staticRating,
            slidingRating, slidingRating
        };
    </JavaScript>

    <ClientPanel>
        <StackPanel Alignment="Center" ItemSpacing="24">
            <RatingComponent Stars="5" Rating="{staticRating}" />
            <SlidingRatingComponent Stars="5" Rating="{slidingRating}" />
        </StackPanel>
    </ClientPanel>
</App>