📜 ⬆️ ⬇️

Application architecture on the Akili framework


Greetings Today I would like to take an example of the implementation of the site architecture on the Akili framework. This article will discuss not so much the system of components as a complete implementation of the application, using routing, ajax requests, storage, etc. right out of the box, without external dependencies.



To build the project, use the webpack , to compile babel with the env , stage-2 , stage-3 presets, to return the static node + express + akili-connect . Eslint is present.


File structure and description


Folder to access statics from the browser / public / assets .
Folder with frontend / src , entry point /src/main.js .
Raising the server in app.js.


There is no backend as such in the example. A simple implementation of static feedback + a couple of lines for server rendering has been written in the app.js file.


Sample data taken from the site https://jsonplaceholder.typicode.com/ .


The frontend structure consists of three main parts:



And three minor:



At the same time, the three folders above with statics are not unique and each individual component can have its own.


The universal (simple) component is completely independent. Data is transmitted to it through attributes , and back we get the result through events . It should not work with the repository . This is done by the controller components. The controller is the link between the repository and simple components.


src / main.js


The best way to register components in a framework is to describe the static .define () method on each component, and call them at the entry point.


import App from './controllers/app/app'; import Posts from './controllers/posts/posts'; import PostEdit from './controllers/post-edit/post-edit'; import Users from './controllers/users/users'; import PostCards from './components/post-cards/post-cards' import PostForm from './components/post-form/post-form' import UserCards from './components/user-cards/user-cards' App.define(); Posts.define(); PostEdit.define(); Users.define(); PostCards.define(); PostForm.define(); UserCards.define(); 

In order to make ajax requests we use the request service.


 import request, { Request } from 'akili/src/services/request'; 

 request.addInstance('api', new Request('https://jsonplaceholder.typicode.com', { json: true })); 

Pay attention to an interesting detail. By default, the request object itself is an instance of the Request class. And by using it one could make any requests. But it is much more convenient to create a separate copy for each direction of requests with its own settings. In this case, we have created a separate one for working with api jsonplaceholder.typicode.com .


Now we can use it anywhere by importing only the request object, for example:


 request.use.api.get('/posts').then(res => console.log(res.data)); 

The request will be sent to https://jsonplaceholder.typicode.com/posts , in the headers with the content type json , and in the answer we will immediately get an object, instead of a string.
More details about ajax requests here .


Further in our file we see the following lines:


 import store from 'akili/src/services/store'; 

 window.addEventListener('state-change', () => store.loader = true); window.addEventListener('state-changed', () => store.loader = false); 

Let's start with the store object. This is the repository of our application. Here you can store any data. At the same time, this storage is automatically synchronized with all the places where any changes are needed. You just need to change the required property. In the lines above, we just, under certain events, change the loader property, to which one of the components is signed, which displays the preloader.


Events state-change and state-changed are not standard for window . They are caused by the framework framework router . The first, before any change in the address bar of the browser, the second, immediately after it. This is what we need for the preloader to work. More on this later.


Next, the router and the framework are initialized after loading the DOM.


 document.addEventListener('DOMContentLoaded', () => { router.init('/app/posts', false); Akili.init().catch((err) => console.error(err)); }); 

src / controllers / app / app.js


This file describes the root controller. In it, we denote the first level of routing to display the header in the template and the entry point for nested routes.


 import './styles/app.scss' import Akili from 'akili'; import router from 'akili/src/services/router'; export default class App extends Akili.Component { static template = require('./app.html'); static define() { Akili.component('app', this); router.add('app', '^/app', { component: this, title: 'Akili example site' }); } compiled() { this.store('loader', 'showLoader'); this.store('posts', posts => this.scope.post = posts.find(p => p.selected)); } } 

Let's go through the code above. First, we load the styles for this component. All static files of a particular component, styles, images, fonts, are stored in its personal folder / src / controllers / app , and not in common.


Next comes the component declaration. The .define () method is optional, but it is a very convenient way to configure each individual component. In it, we describe all the actions that are necessary for the work, and then call it at the entry point (src / main.js).


 Akili.component('app', this); 

The line above registers the component under the app tag so that we can use it in the template. Next is the addition of the route to the router , etc.


.compiled () is one of the lifecycle methods of the component, which is called after compilation. There are two subscriptions to the repository. We spoke about one of them earlier:


 this.store('loader', 'showLoader'); 

With this line we linked the loader storage properties and the scoop property of the current showLoader component. By default, a link is created in both directions. If the store.loader changes, we will get the changes in scope.showLoader and vice versa.


src / controllers / app / app.html


Here is the app component controller template.
We specified it as a static template property in the component.


 static template = require('./app.html'); 

Consider an interesting piece from the template:


 <img src="./img/logo.svg" width="60" class="d-inline-block align-middle mr-1 ${ utils.class({loader: this.showLoader}) }" > 

This is a logo image. It is also a preloader. If you add a loader class to it, the image will start spinning. Now the whole chain of events related to the preloader should be clear. In src / main.js, we subscribed to two events. Before changing the address bar, we change store.loader to true . At this point, the showLoader property in the App component's scoop will also become true , and the expression utils.class ({loader: this.showLoader}) will return the loader class. When the download is complete, everything will change to false and the class will disappear.


Another important piece:


 <div class="container pb-5"> <route></route> </div> 

route is a special component into which the template of the route corresponding to the nesting level is loaded. In this case, this is the second level. That is, any route-heir from the app will be loaded here. And the app itself was loaded into the route , which was specified in body in /public/main.html .


src / controllers / posts / posts.js


It describes the component controller posts.


 import Akili from 'akili'; import router from 'akili/src/services/router'; import store from 'akili/src/services/store'; import { getAll as getPosts } from '../../actions/posts'; export default class Posts extends Akili.Component { static template = require('./posts.html'); static define() { Akili.component('posts', this); router.add('app.posts', '/posts', { component: this, title: 'Akili example | posts', handler: () => getPosts() }); } created() { this.scope.setPosts = this.setPosts.bind(this); this.scope.posts = store.posts; } setPosts(posts = []) { store.posts = this.scope.posts = posts; } } 

Much you already know, but there are new moments. For example, to indicate nesting we use a dot in the name of the route: app.posts . Now posts is inherited from the app .


Also, when announcing the route, we specified the handler function. It will be called if the user gets to the appropriate url. In it, as an argument, a special object will be passed, where all information about the current transit is stored. What we return in this function will also fall into this object. The link to the transit object is in router.transition and is available everywhere.


In the example above, we took data from the repository:


 this.scope.posts = store.posts; 

Because our function .getPosts () at the same time saved it there, but we could take data from the transit:


 this.scope.posts = router.transition.path.data; 

You can see this option in the users controller .


I would also like to note that the component's methods are not in the scope of its template. To call a function in a template, you need to add it to the template scop:


 this.scope.setPosts = this.setPosts.bind(this); 

src / controllers / posts / posts.html


This is a post template. The main task here is to display a list of posts. But since this component is a controller, we will not do it directly here. After all, the list of posts is something universal, we should be able to use it anywhere. Therefore, it is placed in a separate component src / components / post-cards .


 <post-cards data="${ this.filteredPosts = utils.filter(this.posts, this.filter, ['title', 'body']) }" on-data="${ this.setPosts(event.detail) }" ></post-cards> 

Now we will simply transfer the necessary array to the PostCards component, and it will already display everything as it should. True, we still have a search here.


 <input class="form-control" placeholder="search..." on-debounce="${ this.filter = event.target.value }"> 

 <if is="${ !this.filteredPosts.length }"> <p class="alert alert-warning">Not found anything</p> </if> 

Therefore, the data (this.posts) we pass filtered . The on-debounce custom event. It occurs with a delay on the last keystroke in the input. It would be possible to use the standard on-input, but with a large amount of data it will be much less efficient. About the events in general here .


When changing data inside PostCards , it will trigger a custom on-data event, processing which we save changes to posts in the repository by calling this.setPosts (event.detail) .


src / controllers / post-edit / post-edit.js


It describes the controller component of the post editing page.
It makes no sense to analyze all the code, since in the examples above almost everything is the same. Let us dwell on the differences:


 router.add('app.post-edit', '/post-edit/:id', { component: this, title: transition => `Akili example | ${ transition.path.data.title }`, handler: transition => getPost(transition.path.params.id) }); 

In this route, we specified the dynamic parameter id .
Therefore, in the handler function, we have access to its value in transition.path.params.id . In this case, it is the id of the post to get the desired.


src / controllers / post-edit / post-edit.html


As with the list of posts, here we took the form into a separate component of PostForm , so that you can use it many times.


 <post-form post="${ this.post }" on-save="${ this.savePost(event.detail) }"></post-form> 

src / components / post-form / post-form.js


Consider this component.
Pay attention to the comments:


 /** * Universal component to display a post form * * {@link https://akilijs.com/docs/best#docs_encapsulation_through_attributes} * * @tag post-form * @attr {object} post - actual post * @scope {object} post - actual post * @message {object} post - sent on any post's property change * @message {object} save - sent on form save */ 

This is js-doc with some custom tags.



Comments in the source code of the framework are written in the same style.


 compiled() { this.attr('post', 'post'); } 

In the piece of code above, we created a link between the post attribute and the scopa property of the post component. That is, if you pass this attribute with some value, then we immediately get the changes in scope.post . If you change the scope.post in the component, the on-post event will be automatically triggered.


 <post-form post=”${ this.parentPost }” on-post=”${ this.parentPost = event.detail }”> 

If we wrote the html code above somewhere, then we would have a double connection between the parent scope.parentPost and the current scope.post .


But our form works a little differently. We need to save the changed post only by pressing a button, and not with each change. Therefore, we use our own click event :


 static events = ['save']; 

 save() { this.attrs.onSave.trigger(this.scope.post); } 

In the first line, we registered a custom event. The .save () method is called when a button is clicked on a form. In it, we trigger our registered event save and send a new post.


 <post-form post="${ this.post }" on-save="${ this.savePost(event.detail) }"></post-form> 

This piece of code is from the PageEdit controller template . That is, we passed the post through the post attribute to the PostForm component, and we get back the changed one by processing on-save .


src / actions


Actions are just functions for getting and saving data. For cleanliness and convenience, they are placed in a separate folder.


For example, src / actions / posts.js :


 import request from 'akili/src/services/request'; import store from 'akili/src/services/store'; export function getAll() { if(store.posts) { return Promise.resolve(store.posts); } return request.use.api.get('/posts').then(res => store.posts = res.data); } export function getPost(id) { return getAll().then(posts => { let post = posts.find(post => post.id == id); if(!post) { throw new Error(`Not fount post with id "${id}"`); } return post; }); } export function updatePost(post) { return request.use.api.put(`/posts/${post.id}`, { json: post }).then(res => { store.posts = store.posts.map(item => item.id == post.id? {...item, ...post}: item); return res.data; }); } 

Everything is quite simple. Three functions: to get a list of posts, get a specific post and update the post.


Let's sum up


We will not consider files with user components, since almost all the logic there is similar to the one described above.


In this article, I did not want to describe in detail about all the capabilities of the framework component system, although this is a very important component. There are many examples on the site : tree implementation, todo list, setInterval, tabs , etc. The documentation is also full of examples and quite complete. The main goal was to show you how to quickly and easily create an application on Akili.


What we end up with using Akili:



')

Source: https://habr.com/ru/post/351916/


All Articles