- Rendering views in Javascript
- How to make the HTML?
- Libraries
- The clone/modify camp
- The templating camp
- The “injecting fragments of HTML rendered by a server” camp (eg RJS)
- Going back to MVC
- Item Views vs Container Views
- Making simple item View classes
- Step 1.
- Step 2.
- Step 3.
- Step 4.
- Step 5.
- Updating views
- Chucking in a bit of behaviour
- Container View class
- Events
- Conclusion
- If you enjoyed this post, you might also like:
- Scale up your React front-end with thoughtbot
- Рендер страницы в JavaScript
Rendering views in Javascript
This is possibly the hardest part of a Javascript MVC framework to standardise on. More than anywhere else, it is open to interpretation and differences of opinion. I will briefly run through a number of different options moving towards the kind of thing I like.
One of the key things to think about first when deciding on your javascript strategy is accessibility and SEO. This will determine whether you need the majority of your DOM to be rendered by the server, and whether you are going to present different parts of your application as different “pages”.
If you are going to build a traditional web site where doing things goes to different pages, you can then decide on how much you want to add “Degradable” enhancements into the mix.
I am going to focus on non-degradable functionality, and am tallking about the types of RIAs that would previously have been done in Flash. I’m a firm believer in the capabilities of using HTML 5 to build almost native feeling apps.
How to make the HTML?
With dynamic state changing in your client’s browser, these objects first need to be rendered, and then updated if they change.
The simplest thing that works is often something like this:
document.write(" "+ myObject.name+" ")
Since this is obviously too simple for a real application, the next evolution of this would be to put the new data in a specific place (using JQuery).
$('#my_list_of_objects').append(" "+ myObject.name+" ")
At this point it becomes necessary to choose an overall strategy for updating the UI and the choices can be classed as follows:
- Simple string interpolation
- Injecting fragments of HTML rendered by a server
- Employing a templating language
- Modifying and cloning existing parts of the DOM
Libraries
There are several libraries available for rendering JS objects as HTML, which I will cover briefly.
The clone/modify camp
There are several options:
These work by cloning a fragment of HTML you have rendered with your server, and then replacing parts of it with the attributes of a data object. This often means that an object such as this:
Can be fed into the following HTML and in this case populate the “text” attribute of the span with “max”:
id="my_template" class="name">max
There are often nifty options when piping in the data so that it can bind to changes on the data object itself, or other parts of your UI such as a form.
One of the drawbacks I have found from this sort of library is that customising the view can become difficult. In the example above, it would be difficult to make the following object:
into (note inline style, and id attribute):
id="my_object_3" style="background: #eee;">foo
Flexibility can often be achieved by passing in a bunch of configuration options, but this seems a little unwieldy to me.
The templating camp
These essentially mean that you end up writing something like eRB in Javascript. They aren’t really my favourite option, as I think introducing a templating language into a frontend app violates some sort of long term maintainability principle.
What’s more, we already have enough opinionated people arguing about HAML vs Erb Vs whatever-else in our server side code, without introducing another fight on the client end.
If you like this sort of thing some of them are pretty advanced. I believe Sammy templates can be requested from the server as GET requests, and also cached if necessary.
The “injecting fragments of HTML rendered by a server” camp (eg RJS)
This is not really something I like doing, as it fits into my concept of “Pseudo state”.
If a part of a UI needs to be updated by a change of state, this method means your client app goes crying to Daddy server (via Ajax) to request a snippet of HTML which is rendered for it. It then inserts the HTML often by using some sort of evaluated Javascript which has been sent in the request.
I could go into more detail about this, but it just feels like an anti-pattern, and I can’t really be bothered.
Going back to MVC
Rather than using a library for creating HTML that might mean conforming to data-binding conventions I don’t agree with, I tend go for an approach which is flexible and javascripty. This means building things up from the simplest solution that works, to a case that deals with a lot of complexity.
The technique borrows from the clone-modify pattern, but adding a “builder” type of pattern to modifying it. The end result can be extended to incorporate behaviour and event handling.
Item Views vs Container Views
This is quite interesting distinction, but is worth thinking about. Often you can get away with only using item views.
The easiest way of thinking about it is in terms of Rails’ partials. The following is adding a container view (myfriendlist) to the page, which corresponds to a widget:
%= render :partial => "my_friend_list" %>
In this partial you might have the following erb, which renders an item view (“myfrienditem”) based on a collection of friends:
My friends %= render :partial => "my_friend_item", :collection => @my_friends %>
Instances of item view classes render one or more similar objects and container view classes are the holder for a collection of item views. These aren’t hard and fast rules, but are a kind of principle.
Imagine the following scenario based on a collection of people.
myFriendCollection = [ name: "Max" >, name: "Frodo" > ]
A likely widget on a page might be a list of your friends:
In this example, a “friendListView” class might be in charge of iterating through the “myFriendCollection” array and adding a “friendItemView” to the UL for each person it encounters.
In this way, an instance of “friendItemView” would receive a single object it is to render. If items are added or removed from the collection, the “friendListView” will need to redraw its children.
Making simple item View classes
Here’s how I tend to build-up the complexity of something like the example above. This involves a similar “clone and modify” technique that some other libraries use, but rather than abstracting the data mapping behind a bunch of configuration options, I will simply build it up with basic JQuery. This may not be to everyone’s taste, but I think it strikes a nice balance between complexity and simplicity.
Step 1.
Identify the place in the view where you want to make changes.
Step 2.
Make a view class to represent an item to reside in that list:
var friendItemView = function(holder_elem, person) . >
Note that holder_elem is the DOM element we created earlier ($(‘#my_friend_list’)) , and person is the data object we are passing to it.
Step 3.
Put some logic in the class to draw the object you have been given into the holder element (in this case a simple string):
var friendItemView = function(holder_elem, object) var draw = function() holder_elem.append(" "+ object.name + " "); >; // run when initialised draw() >
Step 4.
Instantiate the class with the holder element and an object to be rendered:
new friendItemView($('#my_friend_list'), person);
Step 5.
If you need to render a more complex template, you can grab it from a “templates” section in your HTML (not related to the above example):
style="display: none" id="templates"> id="my_object_template"> class="description">
This then means altering your class to clone the template, and build the data within it:
var friendItemView = function(holder_elem, person, template) elem = template.clone().attr("id", null); // nullify id so it isn't duplicated var draw = function() elem.find('h3').text(person.name) elem.find('.description').text(person.description) >; // run when initialised draw() >
And changing how the object is initialised:
new friendItemView($('#my_friend_list'), person, $('#my_object_template'));
Updating views
These item based views nicely encapsulate all the data needed to render themselves. However, in these examples, I have used a private method (“draw”) to render them. What happens if the data changes, and the view needs to be re-rendered? It is possible to make “draw” public, so it can be called externally, but that often makes things a bit messy. It leads to the following code being duplicated all over the place, not to mention the fact that something global needs to keep track of these views in order to modify them:
// do some stuff to change the data object // find the view which corresponds with the object // then call: instance_of_specific_view.draw()
Events simplify this greatly. Rather than going into detail on events, I will just give a couple of examples.
In the examples above, there is a one-to-one relationship between a view instance and the data object it is passed. This means that the view can bind to events triggered by the object which is passed to it. Changing the represented state in the HTML is therefore handled internally. Note that entering this territory means maintaining your data objects in sensible “Model” classes (the M in MVC). We use JS-Model to handle this.
The following illustrates an example:
var friendItemView = function(elem, person) . person.bind('update', draw); >
Hopefully you can see that this makes it easy maintain.
Chucking in a bit of behaviour
You can also use the view as a place to bind behaviour to your HTML. The following demonstrates the need to “drag” friends out of the list (kinda pointless on its own):
var friendItemView = function(elem, person) . elem.draggable(); >
Container View class
These are generally less used than the item views and need less code.
If you want a container which draws the collection of objects, it is generally as easy as doing:
var friendListView = function(elem, myFriendCollection) var draw = function() for (i in myFriendCollection) new friendItemView($('#my_friend_list'), myFriendCollection[i], $('#my_object_template')); > > >
Sometimes these holder classes may need to dictate dimensions to their children (such as height), which can only be ascertained by holding knowledge of the collection eg:
. for (i in myFriendCollection) friend_item = new friendItemView($('#my_friend_list'), myFriendCollection[i], $('#my_object_template')); friend_item.height = height_of_container / myFriendCollection.length > .
Events
These collections are more likely to listen to events on the collection such as when a new item is added. On occasion, they may need to purge all the item views and redraw.
var friendListView = function(elem, collection) . var add_new_view = function(evt, object) new friendItemView(elem, object, $('#my_object_template')); > collection.bind('create', add_new_view); >
Conclusion
While this technique is a little more involved than some of the templating languages, it offers far more control and crucially it feels really Javascripty.
If you enjoyed this post, you might also like:
Scale up your React front-end with thoughtbot
We’ll work with you to build your React web app while leveling up your team’s skills to keep the progress going after we’re gone.
© 2023 thoughtbot, inc. The design of a robot and thoughtbot are registered trademarks of thoughtbot, inc. Privacy Policy
Рендер страницы в JavaScript
Начинаем наше погружение в работу фронтенд-библиотек и фреймворков. И даже если вы занимались только бэкендом и мир JavaScript вам казался страшным, то самое время расширить кругозор и заглянуть на его сотрону. Как хороший дизайнер должен представлять, можно ли нарисованную им страницу сверстать, так и хорошему бэкендеру было бы неплохо понимать, что от вас хотят фронтендеры. Да и помимо кругозора там можно подсмотреть многие паттерны, которые для вас могут оказаться полезными.
Так что начнём наше исследование на примере написания с нуля браузерного приложения. И сегодня с классического серверного рендеринга HTML-страниц перейдём на построение DOM-дерева через JavaScript. Заодно рассмотрим пользу отделения данных от представления. Выделим побочные эффекты и сделаем удобные и тестируемые чистые функции:
- 00:00:36 — Вопрос работы с фронтендом
- 00:01:57 — Статическая страница
- 00:04:16 — Корневой элемент
- 00:04:53 — Создание логотипа в JavaScript
- 00:07:31 — Генерация шапки сайта
- 00:08:10 — Вывод текущего времени
- 00:09:26 — Группировка состояния страницы
- 00:10:56 — Вывод лотов
- 00:14:03 — Проблема неструктурированного кода
- 00:14:36 — Декомпозиция на компоненты
- 00:16:13 — Всплытие определений в JavaScript
- 00:17:12 — Концепция чистой функции
- 00:21:15 — Группировка аргументов функции
- 00:23:37 — Раздельный вывод лотов
- 00:25:30 — Компонент приложения
- 00:25:58 — Процедура рендерига
А в следующем эпизоде в нашей свёрстанной странице добавим динамику. Чтобы данные подгружались по API и в реальном времени обновлялись цены через WebSocket.