In the
previous part of the tutorial, we solved the problems of isomorphic routing, navigation, fetching, and the initial data state. In the end, it turned out to be a rather simple and concise basis for an isomorphic application, which I also singled out into a separate repository -
ractive-isomorphic-starterkit . In this part, we’ll start writing the
RealWorld application, but first we’ll do the decomposition. Let's go!

Traditional Offtopic
Interesting results showed a
poll about
State-based routing. Half of those who voted did not appreciate what was expected. Still half the idea is still interested, one way or another.
An interesting fact is that just recently I visited J.Subbotnik on the frontend. The guys from Yandex seem to have the idea of
State-based routing yet, and it pleases. The speaker who touched on this topic was from the team of partner interfaces and, apparently, for them it looks like this:
<Match strict state={{page}} params={{status: 'NEW', query: 'Yandex'}} > <App>...</App> </Match> <Switch> ... </Switch>
Actually, this is not fundamentally different from what I do:
')
{{#if $route.match(page) && status === 'NEW' && query === 'Yandex'}} <App>...</App> {{/if}}
Is that the fact that they use
React , which simply does not have any other means of expression, except for the creation of components on any fir-trees - this is his style.
At the end of the
report, I specifically once again focused on this part. Then it seemed to me that the speaker was so deeply immersed in the specifics of his project that many could not fully grasp his idea.
Decomposition and composition

In brief, in the context of the frontend, decomposition is the process of breaking up a monolithic application into components, each of which solves a part of the problem. The main objectives of the decomposition are to reduce the empirical complexity, as well as the implementation of the
DRY principle.
It is the component approach that dominates the modern frontend and is part of the modular approach as a whole. In other words, we divide the code into modules, and the interface into components. Again, in the context of the frontend, a component is a certain user element that encapsulates some part of the UI, business logic, etc. An important component of the understanding of the components is the principles on which decomposition occurs.
There are many approaches to decomposition. For example,
React uses the principle “everything is a component”, other frameworks consider components in the context of
SOLID , etc. But in general, it all comes down to the desire to make components what is called "
high cohesion ,
loose coupling ".
In spite of the fact that everyone understands this, it is often understood in different ways. If you take two developers, they are very likely to decompose the application in different ways. I will only describe the principles of decomposition, which I adhere to myself.
Frankly speaking, I do not think it is right to “crush” with components and the principles of crushing
React components are not very close to me. As a rule, I single out components based on the following rules:
- Reuse - using the same functionality in different parts of the application;
- Functional purpose is a clearly defined function and business process with a separate life cycle and state;
- Structural function - improving the structure and improving the readability of the code
It seems to me that there are no other reasons to allocate some part of the application into a separate component. Moreover, excessive decomposition, on the contrary, may complicate the understanding of the project and its support. In other words, balance is very important in this matter.
The whole truth about React (not for the faint of heart)I risk to pick up dizlaikov with entering into karma, but still I will express my
purely subjective opinion (!) About some aspects of
React . Or rather, those approaches to which
React declines, and his followers preach.
I will not cry about
JSX and how terrible it is - this has been said a thousand times already. Yes, and the idea of ​​mixing code and markup does not belong to the reactor, but rather
PHP , from which, it seems to me, it migrated into the reactor. Partially.
Here I want to discuss the principles of decomposition, to which
React declines, and their direct influence on the appearance of such things as
Redux and other
Flux . Perhaps you will be surprised, but I affirm that this was the reason for all those “revolutionary” ideas that the reactor brought. At the same time breaking all the
best practices in their path that have developed over the decades of development of the principles of programming.
“So what's the salt? What salt, one ....
React »
I think everything, as it often happens, began with a great idea -
“everything is a component” . This idea is so simple and clear that it can capture people's minds. Excessive enthusiasm for this simplicity led to the fact that, in fact,
React can not do anything else. It has no other means of expressiveness except the creation of components and their composition (here I specifically do not consider
Virtual DOM and other engine compartments, because they are not so important from an architectural point of view).
Like any other initially simple and ideal idea, the idea of ​​a reactor also faced realities. But the creators of
React seemed to be so keen on this idea that they began to endure the excessive complexity not inside the framework (as the others do), but to leave it within the application, keeping the seeming simplicity of their offspring. And any problems
React always answered the same way - just create a component, because everything in your application is a component.
It is clear that the community has adapted to these principles and even loved them. And yet it began to make unnecessary complexity out of its applications, but not into the framework, but into satellite libraries. That is why, as a rule, now we are not writing applications on
React alone. Behind him the train of all kinds of add-ons and crutches will certainly reach.
Separately, I note that I am also not a big fan of such
“all-in-one” solutions like
Angular . I think that the prerogative of frameworks is all that is connected with architectural questions of applications, questions of decomposition and composition, communication between components. But not questions about sending http requests and things like that. For me,
Angular is too much, and
React is too little.
But back to decomposition and the principle of "just create another component." In the end, this whole beautiful, in its essence, idea led to two main things:
- Since any problem is solved by creating a component, there are a lot of components, they are small, so mixing the markup and code does not look too vulgar;
- Due to the strong fragmentation of the application into components, the composition of the components becomes unnecessarily complicated, which complicates the communication between the components of different levels. Especially in combination with the principle of "one-way data flow" . This leads to the fact that communication through a global state becomes the most obvious solution.
Thus, it is precisely the manic observance of the principle of
“everything is a component” and the absence of other tools that led
React first to uncontrollable decomposition, then to complicate the composition of the components, and after that the grass does not grow. You can cross out the generally accepted principle of separation of code and markup, and convince your followers that it's cool when everything is in a heap. It is possible to switch to the use of global states, while for many years we tried to isolate and encapsulate these states. In short, create any madness, if only to preserve the unshakable foundation.
If you do not agree with my opinion or would like to correct me in something, always
welcome in the comment. I generally think that a lively discussion is a much more productive thing than a silent vyser into karma. Thanks in advance for your politeness. I myself did not want to offend or offend anyone.
So, to begin with, let's decompose the main page and the user profile page. I want to realize them first of all.
Home page

Here I have highlighted the relevant components with colored frames:
- The purple frame is the root component (root) or application component;
- The blue frame is a component of the tag list;
- Red frame - component of the list of articles;
- Beige frame - component add to favorites.
Even on the main page there is an embedded pagination component for the list of articles that did not fit into the screenshot.
Notice that the tag component is obviously reusable. As well as the add component to favorites.
User Profile

There are also components of the list of articles, tags and add to favorites. From the new here:
- The green frame is a user profile component;
- The yellow frame is the subscription component of the user.
The pagination component also did not fit into the screenshot, but given that the list of user articles can be long, you should also take it into account here. It becomes obvious that the article list component is also reusable.
I believe that such a division into components is balanced, minimally sufficient to achieve the goals of decomposition. At the same time, the composition of the components remains fairly simple and manageable.
Types of components

It seems to me that there are 3 main types of components:
- Pure components - simple components, the result of which depends entirely on the input parameters (the type of "pure" functions). Perfectly reusable and work well in combination with other components;
- Stand-alone components are complex components that implement some kind of stand-alone functionality and implement the SOLID principles. As a rule, such components are used in the composition with “pure” components, they implement specific business logic, collect data, etc .;
- Wrapper components are not isolated components, which are most often used to improve the structure of templates, transfer parameters down, etc.
As I have said many times, in the real world, things are not so straightforward, so components often have mixed features and this is normal.
Write the code
Root component
The root component or application component is exactly the
Ractive instance we configure and create in
./src/app.js . It also implements the general layout of the application (layout) and contains elements that are always present on the screen (header and basement), as well as the layout of the entire application, including routing.
To improve the structure of the templates and divide the overall layout into smaller parts, we can use
the wrapper components described in the previous section. In
Ractive, we can make a component not isolated by setting a simple property:
{ isolated: false }
However, the components themselves are not “cheap”, because they contain all these reactive and calculated properties, observers, life cycle, etc. In essence, the
Ractive component is a class that has a built-in state and implements some kind of functionality. If our wrapper does not require all this in any way, but is merely a structural element designed to simplify our templates, then it is much “cheaper” to use another built-in decomposition mechanism -
partials .
I already took out the cap and basement in
partials in the previous article. In the same way, I will implement other parts of the layout that do not meet the attributes of the component, because "not everything is a component". ;-)
Total, at this stage, the root template of the application will look like this:
./src/templates/app.html {{>navbar}} {{#with @shared.$route as $route, {delay: 500} as fadeIn, {duration: 200} as fadeOut }} <div class="page"> {{#if $route.match('/login') }} <div class="auth-page" fade-in="fadeIn" fade-out="fadeOut"> Login page </div> {{elseif $route.match('/register') }} <div class="auth-page" fade-in="fadeIn" fade-out="fadeOut"> Register page </div> {{elseif $route.match('/profile/:username/:section?') }} <div class="profile-page" fade-in="fadeIn" fade-out="fadeOut"> Profile page </div> {{elseif $route.match('/') }} <div class="home-page" fade-in="fadeIn" fade-out="fadeOut"> {{>homepage}} </div> {{else}} <div class="notfound-page" fade-in="fadeIn" fade-out="fadeOut"> {{>notfound}} </div> {{/if}} </div> {{/with}} {{>footer}}
Hereinafter I use the
RealWorld project
routing guidelines . In those places where the guides do not contain specific recommendations, I will use the approaches that I consider correct. Also, I will use the
History API routing instead of hash-routing, because we are writing an isomorphic application, and URL fragments are not known to go to the server.
In addition, I have allocated two more
partials - for layout of the main page and for page 404. Here they are:
./src/templates/partials/homepage.html <div class="banner"> <div class="container"> <h1 class="logo-font">conduit</h1> <p>A place to share your knowledge.</p> </div> </div> <div class="container page"> <div class="row"> <div class="col-md-9"> <div class="feed-toggle"> <ul class="nav nav-pills outline-active"> <li class="nav-item"> <a href="/" class-active="$route.pathname === '/'" class="nav-link"> Global Feed </a> </li> </ul> </div> Articles list </div> <div class="col-md-3"> <div class="sidebar"> <p>Popular Tags</p> Tags list </div> </div> </div> </div>
./src/templates/partials/notfound.html <div class="banner"> <div class="container"> <h1 class="logo-font">conduit</h1> <p>404 - Not found</p> </div> </div>
Now you need to register them in the root component config:
./src/app.js partials: { ... homepage: require('./templates/parsed/homepage'), notfound: require('./templates/parsed/notfound') },
Full code ./src/app.js const Ractive = require('ractive'); Ractive.DEBUG = (process.env.NODE_ENV === 'development'); Ractive.DEBUG_PROMISES = Ractive.DEBUG; Ractive.defaults.enhance = true; Ractive.defaults.lazy = true; Ractive.defaults.sanitize = true; Ractive.defaults.data.formatDate = require('./helpers/formatDate'); Ractive.defaults.data.errors = null; Ractive.partials.errors = require('./templates/parsed/errors'); Ractive.use(require('ractive-ready')()); Ractive.use(require('ractive-page')({ meta: require('../config/meta.json') })); const api = require('./services/api'); const options = { el: '#app', template: require('./templates/parsed/app'), partials: { navbar: require('./templates/parsed/navbar'), footer: require('./templates/parsed/footer'), homepage: require('./templates/parsed/homepage'), notfound: require('./templates/parsed/notfound') }, transitions: { fade: require('ractive-transitions-fade'), } }; module.exports = () => new Ractive(options);
I’m also a little messy with the transition animation settings - now they look much better.
Pagination component
This component is a bright representative of pure components. The result of his work is completely based on the input parameters - the attributes of the component.

Visually, this component looks completely standard, but it is important for us to decide which
side effect it will produce and how exactly we will manage this
side effect . In the context of an isomorphic application, and with progressive improvement, the answer to this question is obvious - changing the URL.
We must always remember that we need to be able to navigate between pages, even if JS is disabled. In other words, each page must be submitted with its own URL (link). In addition, when reloading the page in the browser, we must remain on the selected list page (full support for
SSR ).
Since there are no recommendations in guidelines about how pagination should be reflected in the URL and whether it should be at all, I use the
URL Query parameter
offset , which will contain the offset in the list. Why
offset , not
page ? This will be easier, because this is exactly how pagination in the API works.
?offset=20
Well, let's create our first component
Ractive . For this, the
Ractive constructor provides us with a static
extend () method, which allows us to extend the constructor with new properties and overwrite existing ones, and as a result get a new constructor. Simply put, this is inheritance.
./src/components/Pagination.js const Ractive = require('ractive'); module.exports = Ractive.extend({ template: require('../templates/parsed/pagination'), attributes: { required: ['total'], optional: ['offset', 'limit'] }, data: () => ({ total: 0, limit: 10, offset: 0, isCurrent(page) { let limit = parseInt(this.get('limit')), offset = parseInt(this.get('offset')); return offset === ((page * limit) - limit); }, getOffset(page) { return (page - 1) * parseInt(this.get('limit')); } }), computed: { pages() { let length = Math.ceil(parseInt(this.get('total')) / parseInt(this.get('limit'))); return Array.apply(null, { length }).map((p, i) => ++i);; } } });
This component accepts the attributes
total (total number of elements in the list),
limit (number of elements on the page) and
offset (current offset in the list). Based on these properties, the component generates a list of pages, which is implemented as a calculated property
pages . In this case, if any of the dependent properties is changed over time, the calculated property will be automatically recalculated. Conveniently.
./src/templates/pagination.html {{#if total > 0 && pages.length > 1}} <nav> <ul class="pagination"> {{#each pages as page}} <li class-active="isCurrent(page)" class="page-item"> <a href="?{{ @shared.$route.join('offset', getOffset(page)) }}" class="page-link"> {{ page }} </a> </li> {{/each}} </ul> </nav> {{/if}}
In the template, we simply display this list as links. Note the use of the
join () special method of the router. This method merges the passed parameter and its value with the current
URL query , as a result we get a ready query string, taking into account the parameters present there. All work on the processing of links, as always, the router itself undertakes and we do not need to worry about it.
It turned out a rather small and simple component, the only
side effect of which is a change in the URL parameter. This allows you to use this component in the composition with any list. The component implementing the list simply subscribes to the change of the corresponding URL parameter and uses this value for API requests and data output.
Component Tags
This component is also clean. However, unlike the
Pagination , it has a different background for this.

The picture shows that the
Tags component is really used in many places in the application. It is also obvious that this component works with a certain list of tags. But the most important thing is that it immediately becomes clear that the list of tags depends on the context in which the component is executed. On the main page - this is a list of popular tags, inside the list of articles - these are tags of a particular article, and so on. That is why this component simply cannot be autonomous and requires the transfer of a list of articles to it from the context in which it is applied.
./src/components/Tags.js const Ractive = require('ractive'); module.exports = Ractive.extend({ template: require('../templates/parsed/tags'), attributes: { required: ['tags'], optional: ['skin'] }, data: () => ({ tags: [], skin: 'outline' }) });
./src/templates/tags.html {{#await tags}} <p>Loading...</p> {{then tags}} <ul class="tag-list"> {{#each tags as tag}} <li> <a href="/?tag={{ tag }}" class="tag-pill tag-default tag-{{~/skin}}"> {{ tag }} </a> </li> {{/each}} </ul> {{catch errors}} {{>errors}} {{else}} <p>No tags</p> {{/await}}
This component is even easier. It takes a list of tags
tags and an additional
skin parameter — the tag style (
outline and
filled ).
Tags can take a list of tags in the form of an array, as well as a promise and independently resolve it to the list of tags. It produces the same
side effect as
Pagination - it changes the query parameter
tag (again, there are no recommendations in the guidelines). It has no dependencies and can be used anywhere in the application.
Pro side effectIt is worth paying special attention that, unlike the pagination component, this component does not merge the parameter being modified to the rest of the query string, but completely updates it. The fact is that by clicking on the tag, the user will be able to see the updated list of articles that correspond to this tag. Thus, possible manipulations with the previous list, for example, navigating through pages, should be reset. At the same time, pagination will work along with the tag refinement, since the Pagination component maintains its parameter to the existing query string.
Let's try to use this component on the main page. First of all, we need to get a list of popular tags from the API. To do this, the service working with the API already has a ready call and all that remains is to write the code that will implement this request.
It is also very important to keep in mind the support for SSR and other isomorphic pieces described in the previous article .And now an interesting point. I most often implement requests for data retrieval with the help of attention, computed properties of components!Why?
I am sure you will understand this by the example of the Articles and Profile components . If briefly - this is how convenient it is!So, we write a simple function that simply returns the value:./src/computed/tags.js const api = require('../services/api'); module.exports = function() { const key = 'tagsList', keychain = `${this.snapshot}${this.keychain()}.${key}`; let tags = this.get(keychain); if ( ! tags) { tags = api.tags.fetchAll().then(data => data.tags); this.wait(tags, key); } return tags; };
As you can see nothing complicated. The function of a computed property is executed in the context of the component to which it is connected. Everything that happens here has already been clarified in the previous section . In addition, we finally used the keychain () method from the ractive-ready plugin . This method simply returns the correct path within the data object, depending on what level of nesting is the component that connected this calculated property.Now we connect this property to the Root component, connect the Tags component and pass this property to it as an attribute../src/app.js ... Ractive.defaults.snapshot = '@global.__DATA__'; ... components: { tags: require('./components/Tags'), }, computed: { tags: require('./computed/tags') }, ...
Full code ./src/app.js const Ractive = require('ractive'); Ractive.DEBUG = (process.env.NODE_ENV === 'development'); Ractive.DEBUG_PROMISES = Ractive.DEBUG; Ractive.defaults.enhance = true; Ractive.defaults.lazy = true; Ractive.defaults.sanitize = true; Ractive.defaults.snapshot = '@global.__DATA__'; Ractive.defaults.data.formatDate = require('./helpers/formatDate'); Ractive.defaults.data.errors = null; Ractive.partials.errors = require('./templates/parsed/errors'); Ractive.use(require('ractive-ready')()); Ractive.use(require('ractive-page')({ meta: require('../config/meta.json') })); const api = require('./services/api'); const options = { el: '#app', template: require('./templates/parsed/app'), partials: { navbar: require('./templates/parsed/navbar'), footer: require('./templates/parsed/footer'), homepage: require('./templates/parsed/homepage'), notfound: require('./templates/parsed/notfound') }, transitions: { fade: require('ractive-transitions-fade'), }, components: { tags: require('./components/Tags'), }, computed: { tags: require('./computed/tags') } }; module.exports = () => new Ractive(options);
./src/templates/partials/homepage.html ... <div class="sidebar"> <p>Popular Tags</p> <tags tags="{{ tags }}" skin="filled" /> </div> ...
Full code ./src/templates/partials/homepage.html <div class="banner"> <div class="container"> <h1 class="logo-font">conduit</h1> <p>A place to share your knowledge.</p> </div> </div> <div class="container page"> <div class="row"> <div class="col-md-9"> <div class="feed-toggle"> <ul class="nav nav-pills outline-active"> <li class="nav-item"> <a href="/" class-active="$route.pathname === '/'" class="nav-link"> Global Feed </a> </li> </ul> </div> Articles list </div> <div class="col-md-3"> <div class="sidebar"> <p>Popular Tags</p> <tags tags="{{ tags }}" skin="filled" /> </div> </div> </div> </div>
That's all, and most importantly the result pleases the eye:Tags are loaded on the server, the Tags component is rendered during SSR and, since the list of tags comes with the initial state of data, the component is successfully hydrogenated on the client without a second request to the API. Shine!Component Articles
The main difference between autonomous and clean components is that for an autonomous component to work, it is sufficient to simply connect it to the parent component and add the corresponding tag to the markup. If necessary, you can pass some settings through the attributes. The same thing works if you need to remove or disable such a component and all related functionality - just stop using it in templates and / or remove it from the parent component.One of these components in our application will be the Articles component . This component implements a rather separate functionality of the list of articles and internally uses other components, such as Tags and Pagination .
Component Articles It is used at least on 2 pages of the application (main and profile) and can display 5 types of articles list, depending on the parameters that are passed to it:- General list of articles
- Personal list of articles for the current user based on his subscriptions
- List of articles filtered to some tag
- List of articles for any user
- List of articles that arbitrary user has added to favorites
Wow!
In addition, all these kinds of list should support pagination, be isomorphic and work without JS.In fact, it is in such situations that autonomous components are a great solution! It allows you to encapsulate all the necessary markup and logic inside the component, putting out only the necessary interface. This eliminates dangerous side effects when using the component in different parts of the application.Let's start with data fetching :./src/computed/articles.js const api = require('../services/api'); module.exports = function() { const type = this.get('type'), params = this.get('params'); const key = 'articlesList', keychain = `${this.snapshot}${this.keychain()}.${key}`; let articles = this.get(keychain); if (articles) { this.set(keychain, null); } else { articles = api.articles.fetchAll(type, params); this.wait(articles, key); } return articles; };
As you can see, I again wrote a function for the calculated property and it is very similar to the one for tags. Read about the advantages of calculated properties voluntarily under the spoiler.Benefits of the approach, , . ?
-, , :
-, «» :
<!-- , --> {{#if baz}} {{foo}} {{/if}} <!-- , --> {{#if baz}} {{bar}} {{/if}}
-, :
-, ( ) . . :
computed: { foo: require('./computed/baz'), bar: require('./computed/baz'), }
, , , . , 10 -
Articles … ?
Next, we describe the component itself:./src/components/Articles.js const Ractive = require('ractive'); module.exports = Ractive.extend({ template: require('../templates/parsed/articles'), components: { pagination: require('./Pagination'), tags: require('./Tags'), }, computed: { articles: require('../computed/articles') }, attributes: { optional: ['type', 'params'] }, data: () => ({ type: '', params: null }) });
Here we hooked up the nested components, the computed property and decided on the interface of the component - it accepts only two attributes that are not mandatory: type (the type of the list can be either an empty string or 'feed') and params (an object with filtering parameters). The template turned out a bit more complicated, because in fact the component is not small:./src/templates/articles.html <div class="articles-list"> {{#await articles}} <div class="article-preview"> <p>Loading articles...</p> </div> {{then data}} {{#each data.articles as article}} <div class="article-preview"> <div class="article-meta"> <a href="/profile/{{ article.author.username }}"> <img src="{{ article.author.image }}" /> </a> <div class="info"> <a href="/profile/{{ article.author.username }}" class="author"> {{ article.author.username }} </a> <span class="date">{{ formatDate(article.createdAt) }}</span> </div> </div> <a href="/article/{{ article.slug }}" class="preview-link"> <h1>{{ article.title }}</h1> <p>{{ article.description }}</p> <span>Read more...</span> <tags tags="{{ article.tagList }}"/> </a> </div> {{else}} <div class="article-preview"> <p>No articles are here... yet.</p> </div> {{/each}} <pagination total="{{ data.articlesCount }}" offset="{{ @shared.$route.query.offset || 0 }}" limit="20" /> {{catch errors}} <div class="article-preview"> {{>errors}} </div> {{else}} <div class="article-preview"> <p>No articles are here... yet.</p> </div> {{/await}} </div>
Well, let's splash it on the main page../src/app.js components: { ... articles: require('./components/Articles'), },
Full code ./src/app.js const Ractive = require('ractive'); Ractive.DEBUG = (process.env.NODE_ENV === 'development'); Ractive.DEBUG_PROMISES = Ractive.DEBUG; Ractive.defaults.enhance = true; Ractive.defaults.lazy = true; Ractive.defaults.sanitize = true; Ractive.defaults.snapshot = '@global.__DATA__'; Ractive.defaults.data.formatDate = require('./helpers/formatDate'); Ractive.defaults.data.errors = null; Ractive.partials.errors = require('./templates/parsed/errors'); Ractive.use(require('ractive-ready')()); Ractive.use(require('ractive-page')({ meta: require('../config/meta.json') })); const api = require('./services/api'); const options = { el: '#app', template: require('./templates/parsed/app'), partials: { navbar: require('./templates/parsed/navbar'), footer: require('./templates/parsed/footer'), homepage: require('./templates/parsed/homepage'), notfound: require('./templates/parsed/notfound') }, transitions: { fade: require('ractive-transitions-fade'), }, components: { tags: require('./components/Tags'), articles: require('./components/Articles'), }, computed: { tags: require('./computed/tags') } }; module.exports = () => new Ractive(options);
./src/templates/partials/homepage.html ... <li class="nav-item"> <a href="/" class-active="$route.pathname === '/' && ! $route.query.tag" class="nav-link"> Global Feed </a> </li> {{#if $route.query.tag }} <li class="nav-item"> <a class="nav-link active"> # {{ $route.query.tag }} </a> </li> {{/if}} ... <articles params="{{ $route.query }}"/> ...
Full code ./src/templates/partials/homepage.html <div class="banner"> <div class="container"> <h1 class="logo-font">conduit</h1> <p>A place to share your knowledge.</p> </div> </div> <div class="container page"> <div class="row"> <div class="col-md-9"> <div class="feed-toggle"> <ul class="nav nav-pills outline-active"> <li class="nav-item"> <a href="/" class-active="$route.pathname === '/' && ! $route.query.tag" class="nav-link"> Global Feed </a> </li> {{#if $route.query.tag }} <li class="nav-item"> <a class="nav-link active"> # {{ $route.query.tag }} </a> </li> {{/if}} </ul> </div> <articles params="{{ $route.query }}"/> </div> <div class="col-md-3"> <div class="sidebar"> <p>Popular Tags</p> <tags tags="{{ tags }}" skin="filled" /> </div> </div> </div> </div>
Notice how the addition of the tab with the tag name, which is filtered, is implemented. If a user clicks on some tag from the Tags component (and it doesn't matter from the list of articles or popular tags), the list of articles is not only filtered by this tag, but also a tab with the tag name is added to visually highlight it.In general, it works like this and, in my opinion, it turned out not bad:And of course, everything is isomorphic, the initial loading occurs without a single ajax-request on the client. The browser history is fully functional, and with JS turned off everything works fine. In short, we dig a bit further.Profile component
I would like to say that this component will be something outstanding, but no. This is plus or minus the same autonomous component as Articles and it will work plus or minus as well. In fact, it is even more boring, as it is used only on one page.
In fact, it is this page. I do not know how to put it another way../src/components/Profile.js const Ractive = require('ractive'); module.exports = Ractive.extend({ template: require('../templates/parsed/profile'), components: { articles: require('./Articles') }, computed: { profile: require('../computed/profile') }, attributes: { required: ['username'], optional: ['section'] }, data: () => ({ username: '', section: '' }) });
However, the traditional computed property is still a bit complicated:./src/computed/profile.js const api = require('../services/api'); let _profile; module.exports = function() { const username = this.get('username'); const key = 'profileData', keychain = `${this.root.snapshot}${this.keychain()}.${key}`; let profile = this.get(keychain); if (profile) { this.set(keychain, null); _profile = profile; } else if (_profile && _profile.username === username) { profile = _profile; } else if (username) { profile = api.profiles.fetch(username).then(data => (_profile = data.profile, _profile)); this.wait(profile, key); } return profile; };
Here, I kind of “cache” the user profile in the closure ( _profile ), because I don’t want to re-request the user profile when I switch to Favorited Articles and back. It is not difficult and not expensive, but it works well. For example, in the implementation of React / Redux this issue is not resolved and therefore, every time a transition is made between “My Articles” and “Favorited Articles”, the profile is fetched. Immediately in sight the guys did not try.Now all this economy is used in the template:./src/templates/profile.html <div class="profile"> {{#await profile}} {{then profile}} <div class="user-info"> <div class="container"> <div class="row"> <div class="col-xs-12 col-md-10 offset-md-1"> <img src="{{ profile.image }}" class="user-img" /> <h4>{{ profile.username }}</h4> <p>{{ profile.bio }}</p> </div> </div> </div> </div> {{catch errors}} {{>errors}} {{/await}} {{#if username}} <div class="container"> <div class="row"> <div class="col-xs-12 col-md-10 offset-md-1"> <div class="articles-toggle"> <ul class="nav nav-pills outline-active"> <li class="nav-item"> <a href="/profile/{{ username }}" class-active="! section" class="nav-link"> My Articles </a> </li> <li class="nav-item"> <a href="/profile/{{ username }}/favorites" class-active="section === 'favorites'" class="nav-link"> Favorited Articles </a> </li> </ul> </div> <articles params="{{ section === 'favorites' ? {favorited: username} : {author: username} }}" /> </div> </div> </div> {{/if}} </div>
Then everything is as usual - we add it to the Root component and to the corresponding route../src/app.js components: { ... profile: require('./components/Profile'), },
Full code ./src/app.js const Ractive = require('ractive'); Ractive.DEBUG = (process.env.NODE_ENV === 'development'); Ractive.DEBUG_PROMISES = Ractive.DEBUG; Ractive.defaults.enhance = true; Ractive.defaults.lazy = true; Ractive.defaults.sanitize = true; Ractive.defaults.snapshot = '@global.__DATA__'; Ractive.defaults.data.formatDate = require('./helpers/formatDate'); Ractive.defaults.data.errors = null; Ractive.partials.errors = require('./templates/parsed/errors'); Ractive.use(require('ractive-ready')()); Ractive.use(require('ractive-page')({ meta: require('../config/meta.json') })); const api = require('./services/api'); const options = { el: '#app', template: require('./templates/parsed/app'), partials: { navbar: require('./templates/parsed/navbar'), footer: require('./templates/parsed/footer'), homepage: require('./templates/parsed/homepage'), notfound: require('./templates/parsed/notfound') }, transitions: { fade: require('ractive-transitions-fade'), }, components: { tags: require('./components/Tags'), articles: require('./components/Articles'), profile: require('./components/Profile'), }, computed: { tags: require('./computed/tags') } }; module.exports = () => new Ractive(options);
./src/templates/app.html ... {{elseif $route.match('/profile/:username/:section?') }} <div class="profile-page" fade-in="fadeIn" fade-out="fadeOut"> <profile username="{{ $route.params.username }}" section="{{ $route.params.section }}" /> </div> ...
Full code ./src/templates/app.html {{>navbar}} {{#with @shared.$route as $route, {delay: 500} as fadeIn, {duration: 200} as fadeOut }} <div class="page"> {{#if $route.match('/login') }} <div class="auth-page" fade-in="fadeIn" fade-out="fadeOut"> Login page </div> {{elseif $route.match('/register') }} <div class="auth-page" fade-in="fadeIn" fade-out="fadeOut"> Register page </div> {{elseif $route.match('/profile/:username/:section?') }} <div class="profile-page" fade-in="fadeIn" fade-out="fadeOut"> <profile username="{{ $route.params.username }}" section="{{ $route.params.section }}" /> </div> {{elseif $route.match('/') }} <div class="home-page" fade-in="fadeIn" fade-out="fadeOut"> {{>homepage}} </div> {{else}} <div class="notfound-page" fade-in="fadeIn" fade-out="fadeOut"> {{>notfound}} </div> {{/if}} </div> {{/with}} {{>footer}}
Fuh, for today perhaps enough. The current results of the project are here:→ Repository→ DemoIn the next part we will work on authorization and isomorphic forms with progressive enhancement . It will be interesting, do not switch!