Welcome to the Wix Declarative Frontend workshop.
Today we will explore some alternatives to $w
by adding some functionality to a Todo App using declarative libraries. Let's get started!
You will add filtering and count functionality to an existing TodoApp in 3 different variations:
- The imperative way
- Using
react-velo
- Using
velo-mvvm
- Open this template (make sure you are logged into Wix)
- Preview and get a feel for how the app works. Add some Todos, mark them as completed, delete some Todos, play around.
- Select different Filters on the left - currently, changing the filter does nothing. Observe the "Items Left: 0" counter. Currently, it does not update when you change the state of your Todo items. Let's change that and make it work!
- Exit Preview, turn on Developer Mode and take a look at the code.
The site has 3 pages: imperative
, react-velo
, velo-mvvm
.
In each page's code there are TODO comments instructing you to add the missing functionality. Feel free to complete the tasks in whatever order you'd like.
If you look at the public
folder you will see a todo-helpers.js
file there.
It exposes some useful functions to create, filter and sort Todos.
The functions are already imported in all of the pages and you can use them.
If you want to add some additional utility functions, you are welcome to do so, but note that no additional function is required to complete the tasks of this workshop.
Imperative programming is all about the How.
When something in our program happens (e.g. a user interaction, a state change) we need to instruct the program exactly what needs to happen as a result.
Let's start by implementing the filter functionality.
The nickname of our Filter Radio Group is #filterGroup
.
The values it can have are "all" | "completed" | "notCompleted"
. You can click on it in the editor and then click on Manage Choices in the floating panel to see it for yourself.
A Radio Group has an onChange
function that takes a callback function that will be triggered any time the value changes. You can read about it in the docs here.
Implement the filtering functionality.
Hint
In imperative programming, we want to think about how changes in state/UI effect other pieces of our program.
What needs to change when the filterGroup
value changes?
The #todoList
repeater.
Solution
filterGroup.onChange((e) => {
filter = e.target.value
// updateList is a helper function located at the bottom of this file.
// Take a look at its implementation
// It updates the list by the selected filter.
updateList()
})
#notCompletedCount
is a Text element that shows the number of items that are not completed. Implement its behavior.
Hint
This counter should update in the following scenarios:
- When a new item is added.
- When an existing item is marked as completed.
- When an existing item is deleted.
Since this update needs to happen in multiple places, start by writing an updateNotCompletedCount()
function that will update the element's text
to the correct value.
Then, call this function in all of the scenarios listed above.
Solution
todoInput.onKeyPress(e => {
//... existing code, and then
updateNotCompletedCount()
})
doneCheckbox.onChange(() => {
//... existing code, and then
updateNotCompletedCount()
})
xButton.onClick(() => {
//... existing code, and then
updateNotCompletedCount()
})
function updateNotCompletedCount() {
notCompletedCount.text = `Items Left: ${
getFilteredTodos(todos, 'notCompleted').length
}`
}
react-velo
brings React to Velo!
You build your UI using the drag-and-drop editor, describe your UI with JSX (your elements are available under the W
namespace) and write React code!
You can read more about react-velo
in the official README here.
More resources on React can be found at the Appendix section of this document.
The filter group is available as a React Component under W.filterGroup
. Add the desired behavior to the filter group so that the list is updated any time the filter selection changes.
Hint
Declarative programming is all about the what. When changing the value of the filterGroup, what needs to change?
The value of the filter state.
Note that this piece of state is already added for you (line 11) and that it is initially set to all
.
By leveraging react-hooks we also have a setFilter function that will update this piece of state. Any piece of your component that cares about this state will update automatically - so you only care about what needs to happen, and not how it actually happens.
If you are new to React that's perfectly fine, you will find some great resources in the appendix section to learn more. To get you started, this is how you would write this component using jsx, passing an onChange
Prop (syntactically, Props are just like attributes of an HTML element)
<W.filterGroup onChange={(e) => {
//add your function implementation here
}} />
Solution
<W.filterGroup
onChange={(e) => {
setFilter(e.target.value)
}}
/>
The "notCompleted" text element is available to you as a React component under W.notCompleted
.
Render this component in your app so its text reflects the correct number of Todo Items left to complete.
Hint
This example beautifully exemplifies the power of declarative programming. All you need to do is describe what W.notCompleted
needs to show, you don't care about the various use-cases in which it needs to be updated.
If you are unfamiliar with React, this is what you need to render this component, passing it the text
prop:
<W.notCompletedCount text={`write the desired value of the text here`} />
Solution
<W.notCompletedCount
text={`Items Left: ${getFilteredTodos(todos, 'notCompleted').length}`}
/>
velo-mvvm
is another library that embraces the Declarative Programming Paradigm, but does it differently than react-velo
. It uses mobx
, which is a "Reactive" library, to create an observable state that you can bind your view to. Your view-elements automatically react to changes in the observed state. You can find some great resources on mobx
and Reactive Programming in the Appendix section of this document.
In velo-mvvm
, you create the observable state by calling the createModel
function. You then use bindView
and the special repeaters function bindRepeaters
to bind your view to the state.
You can read more about velo-mvvm
in the official README here.
Add the desired behavior to the filter group so that the list is updated any time the filter selection changes.
The element's nickname is #filterGroup
.
Use the bindView
function from velo-mvvm
.
Hint
bindView
accepts a javascript object. Each property is a nickname, and the value is always a function.
In reactive-programming, functions are first-class citizens. Whenever a relevant piece of the state changes, the bound function will re-run and the element will update.
Let's see where you would add the code for the #filterGroup
RadioGroup:
bindView({
//... the rest of the bindings that already exist
'#filterGroup': {
onChange: (e) => {
// your code here
},
}
})
- Note: You can also call
bindView
multiple times, so feel free to either add it as an additional property to the existing call or call it separately any place that you'd like.
Solution
bindView({
'#filterGroup': {
onChange: (e) => {
model.filter = e.target.value
}
}
})
Note how just like in the react-velo
example, this piece of code does not care what needs to happen in reaction to the state change or how this state transition occurs. It only cares about updating the piece of state it is in charge of, and everything else just works automatically!
The nickname of the "notCompleted" text element is #notCompletedCount
. Implements the code so that it correctly shows the number of items left.
Hint
The #notCompletedCount
element should reflect the number of todos that are not completed. It does not care how and when a new uncompleted item arrives or how an item is transitioned between the completed and uncompleted state. It just needs to perform a calculation, and whenever the relevant pieces of state update, its value will also automatically update!
Once again, declarative programming shines :)
You want to bind the text
value of this element to something, and you can do it by using this piece of code
bindView({
'#notCompletedCount': {
text: () => `write the text you want to be shown here`
}
})
Note: you have a useful getFilteredTodos
function that you can use to get the notCompleted
todos easily.
Solution
bindView({
'#notCompletedCount': {
text: () =>
`Items Left: ${getFilteredTodos(model.todos, 'notCompleted').length}`,
}
})
- React's official site
- React's docs (Highly recommended)
- Thinking in React
- A beginner's guide to React by Kent C. Dodds (egghead.io)