FRP is easy when you can define your "event network" first and then just watch it make profit. For instance, you may define EventStreams and Properties based on DOM events, compose them, do some AJAX and show the result in the UI, just like we did in the Bacon.js Tutorial parts II & III.
Sooner or later, though, you'll run into the "chicken and egg" problem: you need to base your streams on something that will change on the fly. So, you cannot just define your streams based on existing DOM objects, because the DOM will change.
For example, if you want to implement the quite well-known TodoMVC application with Bacon.js, you'll have to solve this problem. I suggest you take a look at TodoMVC yourself, but here's a brief description of the problem. Let's start with a screenshot.
The TodoMVC application is a simple task manager; you add new tasks, mark them complete, edit them and delete them. You have the All, Active and Completed views. Stuff like that.
Assuming you've read the previous log posts and done some Bacon.js coding, you'd hack most of the app together in a couple of hours, save for the tough part. The tough part is the dynamic view, say
TodoListView
, which lists the Todos
that match the current filter (All/Active/Completed). In fact, the only tough part of the view is that the view also allows you to edit and delete the tasks.
Now let's start with the assumption that we want to define a Property, say
todosProperty
that captures the whole data model of the application. The value of this Property will be an Array of Todos, i.e. something like this:[
{ id: 1, title: "Buy Bacon", completed: false },
{ id: 2, title: "Buy Eggs", completed: false }
]
This model would make it very easy to re-render the list whenever the property changes. Also it would be easy to show only completed or active task by applying a filter:
var activeTodos = todosProperty
.map(function(todos) { return _.where({ completed: false })})
The number of active tasks could be derived like this:
var activeTaskCount = activeTodos.map(".length")
To simplify a bit, the
todosProperty
itself would now depend on item additions only, so we could define the property in a very FRP'ish way:var newTodos; // stream of new Todo items
var todosProperty = addedItems.scan([], function(todos, todo) {
return todos.concat([todo])
})
In real life, there are more factors involved, but let's stick to this assumption for now.
If the list was not editable
If the list items were not editable, it would be easy to render the list based on the
todosProperty
:var todosProperty; // a Property containing an Array of Todos
var listElement; // jQuery element
todosProperty.onValue(function(todos) {
listElement.children().remove()
_.each(todos, function(todo) {
var todoElement = renderTodoView(todo)
listElement.append(todoElement)
})
})
We just redraw the list each time anything changes. The list items are not editable so we don't have to capture any data from them. Just render and forget.
If the view was static
On the other hand, if view was static, i.e. the set of displayed items was not changing, things would be quite easy too. Now you could capture the edits for the Todo items with something like this:
var todos; // list of todos
var listElement; // jQuery element
var properties = _.map(todos, function(todo) {
// call renderer function, returns a jQuery element
var todoElement = renderTodoView(todo)
listElement.append(todoElement)
// Use Bacon.UI helpers to create Properties for the "title" and "completed" fields
return Bacon.combineTemplate({
title: Bacon.UI.textFieldValue(todoElement.find(".edit"), todo.title)
completed: Bacon.UI.checkBoxValue(todoElement.find(".toggle", todo.completed)
})
})
// now we convert this list of Properties to a single property
// each value of which is an Array of Properties
var todosProperty = Bacon.combineTemplate(properties)
Nice and easy. Render Todos once, collect editor values into a single Property using a couple of
combineTemplate
calls and we've re-defined the todosProperty
.But it's dynamic and editable
So it happens to be the case that the value of
todosProperty
depends on user's interaction with the DOM elements corresponding each Todo item on the list. And these DOM elements will be added and removed based on changes to the todosProperty
. Classic chicken-egg.
The point of this blog post is to present some of the known solutions to this problem.
Event delegation (a.k.a live bindings)
One way around that chicken-egg problem with dynamically-created views is to use event delegation; bind event handlers to a containing element, and use a specific selector as the second argument to asEventStream. In TodoMVC, we could try something like
var itemEdits = listElement.asEventStream("keyup", ".edit")
This would give us all keyup events from child elements of
listElement
, matching the .edit
selector. Practically we'd get events when the user edits any of the Todo titles on the list.
Now we could re-define
todosProperty
to take these events into account. But before that we'd have to take care of something: we need to include some identifier into the events in order to know which Todo was actually edited. One way to do this would be to include a data-todo-id
attribute to the text input element. This wouldn't be a big deal especially if we used a templating lib like Handlebars.
Assuming the text input element had this data attribute, we could continue like
var itemEdits = listElement.asEventStream("keyup", ".edit")
.map(function(event) {
var textField = $(event.target)
return {
id: textField.attr("data-todo-id"),
newTitle: textField.val()
}
})
Which would give us a stream of all item edits. For example
{ id: 1, newTitle: "Buy a lot of bacon" }
Re-implementing the
todosProperty
could now be done in a number of different ways. For some reason, I like to model this by constructing a number of streams containing modifications to the list, where the modifications are actually functions that take the previous list of Todos and return the modified list. So we might sayvar itemModifications = itemEdits.map(function(edit) {
return function(todos) {
return _.map(todos, function(todo) {
if (todo.id == edit.id) {
return _.extend(_.clone(todo), { title: edit.newTitle})
} else {
return todo
}
})
}
})
This converts the stream of edits into a stream of functions that will transform the list of todos by changing the title of a single todo. We'd also have to convert the stream of new Todos into this form, but I'll leave that as an excercise for you. The
todosProperty
would now look likevar todosProperty = itemModifications.merge(itemAdditions)
.scan([], function(todos, modification) {
return modification(todos)
})
So, for some cases, we can come by the chicken-egg problem by using jQuery smartly. But at least I find this a bit inelegant and not very scalable; speaking in MVC terms, you have to create your Views before you create your Model because the latter depends on the former.
Apply some MVC
At this point I'm struggling not to bake in some smart-ass analogy involving chickens, eggs and bacon. And speaking of MVC, there's another way to get by the chicken-egg problem.
So far we've kept trying to define
todoProperty
in the traditional FRP way, which is, by composing from a fixed set of source streams. This is elegant to some point. But sometimes it may be more practical and arguably more elegant to make a switch from pull to push. And use Bacon.Bus
.
Instead of defining all of the event sources at once, we can introduce a Bus, which allows us to explicitly
push()
events to the stream. It also allows us to plug()
in streams after the creation of todosProperty
.
So let's take a step towards a more traditional MVC architecture and use a stateful model object to represent the list of Todos. The UI can tell this model to change using methods like
addTodo
, removeTodo
and modifyTodo
.
We'll use a variable,
todos
in the list model to hold the current list of Todos.function TodoListModel() {
var todos = []
var changes = new Bacon.Bus()
this.todoProperty = changes.toProperty(todos)
function update(modification) {
todos = modification(todos)
changes.push(todos)
}
this.addTodo = function(newTodo) {
update(function(todos) {
return todos.concat([newTodo])
})
}
this.modifyTodo = function(updatedTodo) {
update(function(todos) {
return _.map(todos, function(todo) {
return todo.id === updatedTodo.id ? updatedTodo : todo
})
})
}
}
This model object now exposes the field
todoProperty
which can be used like in the earlier examples. You might notice that I'm using the exact same functions for the actual list modifications too. The idea is to expose the required set of imperative-style mutator functions that will modify the model's state and push the new list of Todos to the Bus object changes
. The exposed todoProperty
now reflects the current state of the model, which is the latest list pushed to changes
.
Now it's easy to derive Properties like
activeTodos
, completedTodos
, activeTodoCount
etc in the FRP way.
To summarize, we use imperative style for changing state, but expose the state as FRP EventStreams and Properties. One might say that we gain the best of both worlds.
This approach may get easier by using, for instance, an actual Backbone model and expose its state as EventStreams and Properties, as in the Backbone-Bacon TodoMVC list model by Jarno Keskikangas. He's even put a Backbone.EventStreams library on Github. In his own words,
Bacon gives superpowers to Backbone controllers.
Amen.
One step back towards FRP
If you feel uncomfortable with variables (I do), you might want to revise the MVC solution above. Instead of using a variable, we'll use
scan
:function TodoListModel() {
// A Bus of modification functions
var modifications = new Bacon.Bus()
this.todoProperty = modifications.scan([], function(todos, modification) {
return modification(todos)
})
function update(modification) {
modifications.push(modification)
}
// no changes to the rest...
}
So that's the same
scan
approach as in the pure FRP solution.Yet another step towards FRP
This is the way I actually implemented TodoMVC. And submitted a Pull Request too.
Instead of exposing imperative mutators in the model, you can expose corresponding Bus objects. This allows you to plug-in source streams to the model. The model would look like this:
function TodoListModel() {
function mapTodos(f) { return function (todos) { return _.map(todos, f) } }
function modifyTodo(updatedTodo) {
return mapTodos(function (todo) {
return todo.id === updatedTodo.id ? updatedTodo : todo
})
}
function addTodo(newTodo) {
return function (todos) {
return todos.concat([newTodo])
}
}
this.todoAdded = new Bacon.Bus()
this.todoModified = new Bacon.Bus()
var modifications = this.todoAdded.map(addTodo)
.merge(this.todoModified.map(modifyTodo))
this.todoProperty = modifications.scan([], function (todos, modification) {
return modification(todos)
})
}
It the View code you'll plug-in streams to the exposed Buses
todoAdded
and todoModified
. For instance, this is how you might implement a simplified view responsible of rendering a single Todo row.function TodoView(todo, listModel) {
var element = render(todo) // use Handlebars or whatnot
var titleChanges = element.find(".edit").asEventStream("keyup")
.map(".target").map($).map(".val")
var modifications = titleChanges.map(function(title) {
return { id: todo.id, title: title, completed: todo.completed }
})
listModel.todoModified.plug(modifications)
}
One advantage of this approach, compared to the previous one with the imperative mutators is that when you expose the buses
todoAdded
and todoModified
you're also exposing the same things as streams. So now your View components can subscribe to these streams to react to Todo additions and modifications!
There's a little catch here too: you should make sure you apply an "end condition" to the input streams you plug into the model Buses. If you don't do this, the Bus will have references to the streams even after the corresponding UI components have been removed. In practise, you'll probably remove the UI components based on an event in some EventStream, so you can use this same stream for ending the input streams. For example, in
TodoView
you might have a deleted
stream that signals the deletion of this particular Todo. Just add .takeUntil(deleted)
to your modifications stream and it will end on deletion:function TodoView(todo, listModel) {
// .. begins as before ..
var deleted = listModel.todoProperty.changes.filter(function(todos) {
return _.where({ id: todo.id}).length == 0
})
deleted.onValue(function() { element.remove() })
listModel.todoModified.plug(modifications.takeUntil(deleted))
}
You may have a look at the full Bacon.js TodoMVC implementation for reference. I'm sorry for the tab indentation. That's the standard for TodoMVC. Also, don't expect the implementation to be exactly the same as in any of the above examples. But it definitely is based on a TodoListModel that exposes Buses and streams to the Views.
Conclusion
We've actually covered two related problems in the pure FRP approach, both of which stem from having to introduce some Views before the Model because the Model depends on the Views.
- The Chicken-Egg Problem - your Model depends on your Views which depend on the Model
- The Coupling Problem - you have to Introduce some View components before the Model, so you cannot properly modularize your application
The first problem in isolation can be in some cases solved by using techniques like jQuery event delegation. But to address the problem in the bigger picture, you need to apply architectural solutions, such as introducing a Model object with either mutator functions or pluggable Bus objects.
Nice,
ReplyDeleteLearn Digital Academy has been Bangalor's leading training institution in Digital Marketing.
courses in digital marketing in bangalore
thanks for sharing this information
ReplyDeleteaws training center in chennai
aws training in chennai
aws training institute in chennai
best angularjs training in chennai
angular js training in sholinganallur
angularjs training in chennai
azure training in chennai
azure training in sholinganallur
Excellent post!!!. The strategy you have posted on this technology helped me to get into the next level and had lot of information in it.
ReplyDeleteInformatica Online Training | Informatica Online Certification } Learn Informatica Course
All the points you described so beautiful. Every time i read your blog and i am so surprised that how you can write so well.
ReplyDeleteBest MEAN Stack Online Certification
Thanks for sharing such a nice information with us...
ReplyDeleteseo training institute in bangalore
It is amazing and wonderful to visit your site.Thanks for sharing this information,this is useful to me...
ReplyDeletehttp://rexapparels.com/mens-full-sleeve-t-shirt-manufacturer-in-tirupur-india/
http://rexapparels.com/mens-half-sleeve-t-shirt-manufacturer-in-tirupur-india/
http://rexapparels.com/mens-polo-t-shirt-manufacturer-in-tirupur-india/
http://rexapparels.com/mens-printed-t-shirt-manufacturer-in-tirupur-india/
http://rexapparels.com/mens-striped-t-shirt-manufacturer-in-tirupur-india/
After installing the setup.office.com on your computer system, your existing MS account or create a new account and enter the product key. you can visit
ReplyDeleteoffice setup
Thanks for sharing
ReplyDeletehttps://www.omit.in/
Online marketing has undoubtedly captured the market. Gone are the days when offline marketing was sufficient, every business now gains recognition by making an online presence. There are multiple aspects that has to be covered and the best way to learn them is through Digital Marketing Training in Bangalore.
Thank you for sharing.I was really looking for this.
ReplyDeleteSAP training in Kolkata
Best SAP training in Kolkata
SAP training institute in Kolkata
Much obliged to you for sharing.I was truly searching for this.
ReplyDeleteSAP training in Mumbai
Best SAP training in Mumbai
SAP training institute Mumbai
I enjoyed your blog Thanks for sharing such an informative post. We are also providing the best services click on below links to visit our website.
ReplyDeletedigital marketing company in nagercoil
digital marketing services in nagercoil
digital marketing agency in nagercoil
best marketing services in nagercoil
SEO company in nagercoil
SEO services in nagercoil
social media marketing in nagercoil
social media company in nagercoil
PPC services in nagercoil
digital marketing company in velachery
digital marketing company in velachery
digital marketing services in velachery
digital marketing agency in velachery
SEO company in velachery
SEO services in velachery
social media marketing in velachery
social media company in velachery
PPC services in velachery
online advertisement services in velachery
online advertisement services in nagercoil
web design company in nagercoil
web development company in nagercoil
website design company in nagercoil
website development company in nagercoil
web designing company in nagercoil
website designing company in nagercoil
best web design company in nagercoil
web design company in velachery
web development company in velachery
website design company in velachery
website development company in velachery
web designing company in velachery
website designing company in velachery
best web design company in velachery
Thanks for Sharing - ( Groarz branding solutions )
I love the way you write and share your niche! Very interesting and different! Keep it coming!
ReplyDeleteWeb Design Cheltenham
SEO Gloucester
SEO Agency Cheltenham
check Cryptoanime for all types of anime reviews.
ReplyDeleteExcellent article! Your explanation of tackling the "chicken and egg" problem in FRP is insightful and the various approaches you presented provide practical solutions. I appreciate your clear and concise writing style. Well done!
ReplyDeleteData Analytics Courses in Bangalore
BEST EVER IN THE WORLD. YOU MUST TRY THIS !
ReplyDeleteAMAZING WORK GUYS JUST KEEP IT UP !
SITE IS HERE : Best PDO Threads in Gig Harbor
Thanks for sharing such a great information.. It really helpful to me..I always search to read the quality content and finally i found this in you post. keep it up!
ReplyDeleteDigital Marketing Course in Raipur
Enjoyed reading the article above , really explains everything in detail, the article is very interesting and effective. Thank you and good luck in the upcoming articles
ReplyDeleteBest Gynaecology Hospital in Bettiah
Your Blog is very nice.
ReplyDeleteWish to see this much more like this. Thanks for sharing your information!
Best Eye Hospital in Ambikapur
nice post . Thank you for posting something like this
ReplyDeleteForensic Science Course Near Me
I read this post your post so nice and very informative post thanks for sharing this post
ReplyDeleteDigital Marketing Course in Raipur Chhattisgarh
Thank you for sharing such an informative and engaging blog! digital marketing courses in patna with placements
ReplyDelete"Thank you for this informative post. The points you made about really resonated with me. It's refreshing to read such a well-researched and clearly written piece. Keep up the excellent work!"
ReplyDeleteModular Kitchen & Wardrobe in Raipur