Hello. My name is Alexander and I am Vanilla ES5.1 developer in 2018.
This article is a response to the article-response “How to search for users on GitHub without React + RxJS 6 + Recompose” , which showed us how to use SvelteJS.
I suggest looking at one of the options for how this can be implemented without using any dependencies other than the browser. Moreover, GitHub itself stated that they are developing a frontend without frameworks .
We will do the same input displaying the GitHub user dashboard:
This article ignores absolutely all possible practices of modern javascript and web development.
We don’t need to configure and write configs, create an index.html with all the necessary layout:
<!doctype html> <html> <head> <meta charset='utf-8'> <title>GitHub users</title> <link rel='stylesheet' type='text/css' href='index.css'> </head> <body> <div id='root'></div> <div id='templates' style='display:none;'> <div data-template-id='username_input'> <input type='text' data-onedit='onNameEdit' placeholder='GitHub username'> </div> <div data-template-id='usercard' class='x-user-card'> <div class='background'></div> <div class='avatar-container'> <a class='avatar' data-href='userUrl'> <img data-src='avatarImageUrl'> </a> </div> <div class='name' data-text='userName'></div> <div class='content'> <a class='block' data-href='reposUrl'> <b data-text='reposCount'></b> <span>Repos</span> </a> <a class='block' data-href='gistsUrl'> <b data-text='gistsCount'></b> <span>Gists</span> </a> <a class='block' data-href='followersUrl'> <b data-text='followersCount'></b> <span>Followers</span> </a> </div> </div> <div data-template-id='error'><b data-text='status'></b>: <span data-text='text'></span></div> <div data-template-id='loading'>Loading...</div> </div> </body> </html>
If anyone is interested in CSS , it can be viewed in the repository .
We have the most common styles, no css-modules and other scope'ing. We simply mark the components with classes starting with x- and guarantee that there will be no more such in the project. Any selectors write about them.
All that we want from our input field is the debounce-events of its change, as well as the event of the beginning of the input, so that it immediately shows the indication of the load. It turns out like this:
in_package('GitHubUsers', function() { this.provide('UserNameInput', UserNameInput); function UserNameInput(options) { var onNameInput = options.onNameInput, onNameChange = options.onNameChange; var element = GitHubUsers.Dom.instantiateTemplate('username_input'); var debouncedChange = GitHubUsers.Util.delay(1000, function() { onNameChange(this.value); }); GitHubUsers.Dom.binding(element, { onNameEdit: function() { onNameInput(this.value); debouncedChange.apply(this, arguments); } }); this.getElement = function() { return element; }; } });
Here we have used some utilitarian functions, we will go over them:
Since we have no webpack
, no CommonJS
, no RequireJS
, we put everything into objects using the following function:
window.in_package = function(path, fun) { path = path.split('.'); var obj = path.reduce(function(acc, p) { var o = acc[p]; if (!o) { o = {}; acc[p] = o; } return o; }, window); fun.call({ provide: function(name, value) { obj[name] = value; } }); };
The instantiateTemplate()
function gives us a deep copy of the DOM element that will be received by the consumeTemplates()
function from the #templates
element in our index.html
.
in_package('GitHubUsers.Dom', function() { var templatesMap = new Map(); this.provide('consumeTemplates', function(containerEl) { var templates = containerEl.querySelectorAll('[data-template-id]'); for (var i = 0; i < templates.length; i++) { var templateEl = templates[i], templateId = templateEl.getAttribute('data-template-id'); templatesMap.set(templateId, templateEl); templateEl.parentNode.removeChild(templateEl); } if (containerEl.parentNode) containerEl.parentNode.removeChild(containerEl); }); this.provide('instantiateTemplate', function(templateId) { var templateEl = templatesMap.get(templateId); return templateEl.cloneNode(true); }); });
The Dom.binding()
function takes an element, an option, searches for certain data attributes, and performs the actions we need with the elements. For example, for the data-element
attribute, it adds a field to the result with reference to the marked element; for the data-onedit
attribute data-onedit
keyup
and change
handlers to the element with a handle from the options.
in_package('GitHubUsers.Dom', function() { this.provide('binding', function(element, options) { options = options || {}; var binding = {}; handleAttribute('data-element', function(el, name) { binding[name] = el; }); handleAttribute('data-text', function(el, key) { var text = options[key]; if (typeof text !== 'string' && typeof text !== 'number') return; el.innerText = text; }); handleAttribute('data-src', function(el, key) { var src = options[key]; if (typeof src !== 'string') return; el.src = src; }); handleAttribute('data-href', function(el, key) { var href = options[key]; if (typeof href !== 'string') return; el.href = href; }); handleAttribute('data-onedit', function(el, key) { var handler = options[key]; if (typeof handler !== 'function') return; el.addEventListener('keyup', handler); el.addEventListener('change', handler); }); function handleAttribute(attribute, fun) { var elements = element.querySelectorAll('[' + attribute + ']'); for (var i = 0; i < elements.length; i++) { var el = elements[i], attributeValue = el.getAttribute(attribute); fun(el, attributeValue); } } return binding; }); });
Well, delay
deals with the type of debounce we need:
in_package('GitHubUsers.Util', function() { this.provide('delay', function(timeout, fun) { var timeoutId = 0; return function() { var that = this, args = arguments; if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(function() { timeoutId = 0; fun.apply(that, args); }, timeout); }; }); });
It has no logic, only a template that is filled with data:
in_package('GitHubUsers', function() { this.provide('UserCard', UserCard); function UserCard() { var element = GitHubUsers.Dom.instantiateTemplate('usercard'); this.getElement = function() { return element; }; this.setData = function(data) { GitHubUsers.Dom.binding(element, data); }; } });
Of course, doing so many querySelectorAll
every time we change data is not very good, but it works and we put up with it. If it suddenly turns out that because of this, everything is slowing down, we will write data to the saved data-element
. Or we will make another binding function, which itself saves the elements and can read the new data. Or we will support the transfer to the object of options not just static values, the flow of their changes so that the binding can follow them.
We assume that these representations will also be static, will be used only in one place, and the chances that they will have their own logic are extremely small (unlike the user's card), therefore we will not make separate components for them. They will be just templates for the application component.
Let's make a class with the user request method, in which case we can easily replace its instance with a mock / other implementation:
in_package('GitHubUsers', function() { this.provide('GitHubApi', GitHubApi); function GitHubApi() { this.getUser = function(options, callback) { var url = 'https://api.github.com/users/' + options.userName; return GitHubUsers.Http.doRequest(url, function(error, data) { if (error) { if (error.type === 'not200') { if (error.status === 404) callback(null, null); else callback({ status: error.status, message: data && data.message }); } else { callback(error); } return; } // TODO: validate `data` against schema callback(null, data); }); }; } });
Of course, we need a wrapper over XMLHttpRequest . We do not use fetch
because it does not support interrupt requests, nor do we want to communicate with promises for the same reason.
in_package('GitHubUsers.Http', function() { this.provide('doRequest', function(options, callback) { var url; if (typeof options === "string") { url = options; options = {}; } else { if (!options) options = {}; url = options.url; } var method = options.method || "GET", headers = options.headers || [], body = options.body, dataType = options.dataType || "json", timeout = options.timeout || 10000; var old_callback = callback; callback = function() { callback = function(){}; // ignore all non-first calls old_callback.apply(this, arguments); }; var isAborted = false; var request = new XMLHttpRequest(); // force timeout var timeoutId = setTimeout(function() { timeoutId = 0; if (!isAborted) { request.abort(); isAborted = true; } callback({msg: "fetch_timeout", request: request, opts: options}); }, timeout); request.addEventListener("load", function() { var error = null; if (request.status !== 200) { error = { type: 'not200', status: request.status }; } if (typeof request.responseText === "string") { if (dataType !== "json") { callback(error, request.responseText); return; } var parsed; try { parsed = JSON.parse(request.responseText); } catch (e) { callback(e); return; } if (parsed) { callback(error, parsed); } else { callback({msg: "bad response", request: request}); } } else { callback({msg: "no response text", request: request}); } }); request.addEventListener("error", function() { callback({msg: "request_error", request: request}); }); request.open(method, url, true /*async*/); request.timeout = timeout; request.responseType = ""; headers.forEach(function(header) { try { request.setRequestHeader(header[0], header[1]); } catch (e) {} }); try { if (body) request.send(body); else request.send(); } catch (e) { callback({exception: e, type: 'send'}); } return { cancel: function() { if (!isAborted) { request.abort(); isAborted = true; } if (timeoutId) { clearTimeout(timeoutId); timeoutId = 0; } } }; }); });
in_package('GitHubUsers', function() { this.provide('App', App); function App(options) { var api = options.api; var element = document.createElement('div'); // Create needed components var userNameInput = new GitHubUsers.UserNameInput({ onNameInput: onNameInput, onNameChange: onNameChange }); var userCard = new GitHubUsers.UserCard(); var errorElement = GitHubUsers.Dom.instantiateTemplate('error'); var displayElements = [ { type: 'loading', element: GitHubUsers.Dom.instantiateTemplate('loading') }, { type: 'error', element: errorElement }, { type: 'userCard', element: userCard.getElement() } ]; // Append elements to DOM element.appendChild(userNameInput.getElement()); userNameInput.getElement().style.marginBottom = '1em'; // HACK displayElements.forEach(function(x) { var el = x.element; el.style.display = 'none'; element.appendChild(el); }); var contentElements = new GitHubUsers.DomUtil.DisplayOneOf({ items: displayElements }); // User name processing var activeRequest = null; function onNameInput(name) { name = name.trim(); // Instant display of `loading` or current request result if (activeRequest && activeRequest.name === name) { activeRequest.activateState(); } else if (name) { contentElements.showByType('loading'); } else { contentElements.showByType(null); } } function onNameChange(name) { name = name.trim(); // Cancel old request if (activeRequest && activeRequest.name !== name) { activeRequest.request.cancel(); activeRequest = null; } else if (activeRequest) { // same name return; } if (!name) return; // Do new request activeRequest = { name: name, request: api.getUser({ userName: name }, onUserData), // method for `onNameInput` activateState: function() { contentElements.showByType('loading'); } }; activeRequest.activateState(); function onUserData(error, data) { if (error) { activeRequest = null; contentElements.showByType('error'); GitHubUsers.Dom.binding(errorElement, { status: error.status, text: error.message }); return; } if (!data) { activeRequest.activateState = function() { GitHubUsers.Dom.binding(errorElement, { status: 404, text: 'Not found' }); contentElements.showByType('error'); }; activeRequest.activateState(); return; } activeRequest.activateState = function() { userCard.setData({ userName: data.name || data.login, // `data.name` can be `null` userUrl: data.html_url, avatarImageUrl: data.avatar_url + '&s=80', reposCount: data.public_repos, reposUrl: 'https://github.com/' + data.login + '?tab=repositories', gistsCount: data.public_gists, gistsUrl: 'https://gist.github.com/' + data.login, followersCount: data.followers, followersUrl: 'https://github.com/' + data.login + '/followers' }); contentElements.showByType('userCard'); }; activeRequest.activateState(); } } this.getElement = function() { return element; }; } });
We got quite a lot of code, half of which is occupied by initialization of all the components we need, half - the logic of sending requests and displaying the load / error / result. But everything is absolutely transparent, obviously, and we can change the logic in any place, if necessary.
We used the DisplayOneOf
utility utility, which shows one of the given elements, hides the rest:
in_package('GitHubUsers.DomUtil', function() { this.provide('DisplayOneOf', function(options) { var items = options.items; var obj = {}; items.forEach(function(item) { obj[item.type] = item; }); var lastDisplayed = null; this.showByType = function(type) { if (lastDisplayed) { lastDisplayed.element.style.display = 'none'; } if (!type) { lastDisplayed = null; return; } lastDisplayed = obj[type]; lastDisplayed.element.style.display = ''; }; }); });
To make it all work, we need to initialize the templates and throw an App
instance on the page:
function onReady() { GitHubUsers.Dom.consumeTemplates(document.getElementById('templates')); var rootEl = document.getElementById('root'); var app = new GitHubUsers.App({ api: new GitHubUsers.GitHubApi() }); rootEl.appendChild(app.getElement()); }
As you can see, we wrote a lot of code for such a small example. Nobody does all the magic for us, we achieve everything ourselves. We ourselves create the magic that we need if we want it.
Write a stupid and eternal code. Write without frameworks, which means you sleep tight and don't be afraid that tomorrow all your code will be deprecated or not fashionable enough.
This example is too small to write on VanillaJS in principle. I believe that writing on vanilla only makes sense if your project plans to live much longer than any of the frameworks and you will not have the resources to rewrite it entirely.
But if he was still more, this is what we would do again:
HTML templates we would do for modules / components. They would lie in folders with components and instantiateTemplate
would accept the module name plus the template name, not just the global name.
At the moment, all the CSS we have in index.css
, it, obviously, also need to put next to the components.
There is not enough build bundles, we connect all the files with our hands in index.html
, this is not good.
There is no problem to write a script that, according to the list of modules that should be included in the bundles, will assemble all the js, html, css of these modules and make us one js'nik for each bundle. It will be an order of magnitude dumber and easier than setting up a webpack , and after a year find out that there is already a completely different version and you need to rewrite the config and use other bootloaders.
It is advisable to have any flag that would support the js / html / css connection scheme in a huge list in index.html
. Then there will be no assembly delays, and in Sources in chrome you will have each file in a separate tab and no sourcemaps are needed.
This is just one of the options, as it can all be using VanillaJS . In the comments it would be interesting to hear about other uses.
Thanks for attention.
Source: https://habr.com/ru/post/419893/
All Articles