Drag to reorder
In this example we show how to create a drag-to-reorder behaviour on a list of items. This was made possible by the introduction of IdentityKey
property on Each
tag, which takes care of figuring out which items are where after a list has been modified.
Making a component
Since we want to have code that we can reuse in multiple places, we build a custom ux:Class
component for our SortableList
element. We will need to change the order of items that we pass in through ux:Property
, so that list has to be an Observable. The type of the property needs to be object
. Since we are going to make changes to the list inside of our custom component, we need to make a component-local viewmodel (in-line JavaScript in our component) that will hold the business logic and access the list of items.
<Panel ux:Class="SortableList">
<object ux:Property="Items" />
<string ux:Property="Label" />
<JavaScript>
var items = this.Items.inner();
module.exports = {
items: items
};
</JavaScript>
Am important detail to keep in mind when passing Observables through properties is that the property itself is a derived Observable, so we need to use .inner()
to get access to the Observable list.
The visual part of our component is relatively simple: it’s a StackPanel
that stacks the label and list of items. What’s interesting is the Each
tag with IdentityKey="id"
property set. When the items in the data-bound list change, Each
will check the property id
on every item in the list and figure out which items are the same.
<StackPanel ItemSpacing="8">
<Text Value="{ReadProperty Label}" TextColor="#777" FontSize="18" Font="Bold" Margin="8,0" Alignment="CenterLeft" />
<StackPanel ItemSpacing="1">
<Each Items="{items}" IdentityKey="id">
<Item />
</Each>
</StackPanel>
</StackPanel>
Each item in the list is represented by another ux:Class
. It shows the title of the item and includes a handle that we will drag the items by.
<DockPanel ux:Class="Item" Height="56">
<Text Value="{title}" Alignment="CenterLeft" TextColor="#444" Margin="8,0" />
<Panel Width="56" Dock="Right" HitTestMode="LocalBounds">
<StackPanel Width="16" ItemSpacing="2" Alignment="Center">
<Each Count="4">
<Rectangle Height="2" Color="#bbb" CornerRadius="1" />
</Each>
</StackPanel>
</Panel>
<Rectangle ux:Name="underlay" Color="#fff" Opacity="1" CornerRadius="2" Layer="Background" />
</DockPanel>
Our MainView.ux
will hold 3 instances of the SortableList
component, and we will put them inside of a ScrollView
so that we can scroll to see items that are beyond the screen. This raises a challenge when we need to reorder the list, because tapping and then dragging the items will simply make the ScrollView
scroll.
To solve this challenge, we need to pass in the parent ScrollView
via ux:Dependency
and disable UserScroll
on it while we’re reordering items.
<ScrollView ux:Dependency="parentScrollView" />
...
<WhileTrue Value="{reordering}">
<Change parentScrollView.UserScroll="false" />
</WhileTrue>
We will set the variable {reordering}
to true
when any one of the drag handles are Pressed
and set it back to false
when the list item is Released
.
<Pressed>
<Callback Handler="{select}" />
</Pressed>
...
<Released>
<Callback Handler="{deselect}" />
</Released>
Thanks to the IdentityKey
on Each
, an item in a list now gets its LayoutAnimation
triggered when it changes its position in that list. We take advantage of that, and specify how that animation should look. In our example, an item can only be moved by one index at a time, so it makes sense to have a really simple and quick animation.
<LayoutAnimation>
<Move RelativeTo="PositionChange" Vector="1" Duration="0.16" />
</LayoutAnimation>
To change the order of items within our list, we use a WhileHovering
trigger. It works like this: while we have an item Pressed
and we’re moving the pointer (finger) over another list item, the {hover}
callback fires. In there, we will remove the item we originally Pressed
from the list and insert it at the new index.
<WhileHovering>
<Callback Handler="{hover}" />
</WhileHovering>
This is how the UX code for our Item
component looks like when we put all the bits together:
<DockPanel ux:Class="Item" Height="56">
<Text Value="{title}" Alignment="CenterLeft" TextColor="#444" Margin="8,0" />
<Panel Width="56" Dock="Right" HitTestMode="LocalBounds">
<Pressed>
<Callback Handler="{select}" />
</Pressed>
<StackPanel Width="16" ItemSpacing="2" Alignment="Center">
<Each Count="4">
<Rectangle Height="2" Color="#bbb" CornerRadius="1" />
</Each>
</StackPanel>
</Panel>
<LayoutAnimation>
<Move RelativeTo="PositionChange" Vector="1" Duration="0.16" />
</LayoutAnimation>
<WhileHovering>
<Callback Handler="{hover}" />
</WhileHovering>
<WhileTrue Value="{selected}">
<Change underlay.Opacity="0.6" Duration="0.24" />
</WhileTrue>
<Rectangle ux:Name="underlay" Color="#fff" Opacity="1" CornerRadius="2" Layer="Background" />
</DockPanel>
Now we need to implement the business logic in JavaScript so that our callbacks actually do something. As described before, the list reordering logic resides in the hover()
function, while the select()
and deselect()
functions toggle the reordering
state and make sure the right data is in place when necessary.
<JavaScript>
var Observable = require("FuseJS/Observable");
var items = this.Items.inner();
var selected = null;
var reordering = Observable(false);
function select(args) {
if (selected === null) {
selected = args.data.id;
items.forEach(function(x) {
if (x.id === selected) {
x.selected.value = true;
}
});
}
reordering.value = true;
}
function deselect() {
selected = null;
items.forEach(function(x) {
x.selected.value = false;
});
reordering.value = false;
}
function hover(args) {
if (reordering.value === true && selected !== null) {
var from;
var to;
items.forEach(function(item, index) {
if (item.id === selected) {
from = index;
}
if (item.id === args.data.id) {
to = index;
}
});
if (to !== from && to !== undefined) {
var tmp = items.toArray();
var elem = tmp[from];
tmp.splice(from, 1);
tmp.splice(to, 0, elem);
items.replaceAll(tmp);
}
}
}
module.exports = {
items: items,
reordering: reordering,
select: select,
deselect: deselect,
hover: hover
};
</JavaScript>
Using the component
With the SortableList
component done, it’s time we use it in our MainView.ux
!
We start by creating 3 Observable lists of items in JavaScript:
<JavaScript>
var Observable = require("FuseJS/Observable");
function Item(id, title) {
this.id = id;
this.title = title;
this.selected = Observable(false);
}
var morning = Observable(
new Item(1, "Brush your teeth"),
new Item(2, "Take out the trash"),
new Item(3, "Take the stairs")
);
var day = Observable(
new Item(1, "Buy milk"),
new Item(2, "Make an app"),
new Item(3, "Learn something new")
);
var evening = Observable(
new Item(1, "Dinner with mom"),
new Item(2, "Play chess with a friend"),
new Item(3, "Watch TV")
);
module.exports = {
morning: morning,
day: day,
evening: evening
};
</JavaScript>
Next, we add our ScrollView
and assign it a ux:Name
. Inside there, we stack 3 instances of our SortableList
component, pass the ux:Dependency
for ScrollView
and data-bind the respective lists of items.
<ScrollView ux:Name="parentScrollView">
<StackPanel Margin="8,16" ItemSpacing="24">
<SortableList Items="{morning}" parentScrollView="parentScrollView" Label="Morning" />
<SortableList Items="{day}" parentScrollView="parentScrollView" Label="Day" />
<SortableList Items="{evening}" parentScrollView="parentScrollView" Label="Evening" />
</StackPanel>
</ScrollView>
And now we can reorder lists by dragging items! Go ahead, download the example and make your own drag-to-reorder components!