nova-frontend
Version:
Nova is an alternative to all those gigantic front-end frameworks, that often do more than is necessary when it comes to building simple UIs. Pure Vanilla Javascript is performance-wise the best way to build your front-end in a SPA, but it can be hard to
290 lines (271 loc) • 9.31 kB
JavaScript
/**
* @name Component
* @class
* @desc
* The component is a wrapper for Elements. A component is basically a block of Elements.
* The best way to create a Component is to use the Generator.
* You can also supply it with an array of Elements.
* @example
* import { Generator } from 'nova';
* const generator = new Generator();
* const header = generator.createTree(`
* header className: 'header'
* h1 className: 'header__title' innerText: 'Hello World!'
* h2 className: 'header__subtitle' innerText: 'This is my site.'
* end`)
*
* header.render(); //header is the component
*/
module.exports = class Component {
#arrayOfElements;
#parent
#stateUpdaterArray
constructor(arrayOfElements, parent) {
this.#arrayOfElements = arrayOfElements;
this.#parent = parent || this.#arrayOfElements[0].parent;
this.#stateUpdaterArray = [];
}
/**
* @returns {ArrayOfElements}
*/
get elements() {
return this.#arrayOfElements;
}
/**
* @desc
* Set props of a generated component using Generator.
* When generating the component, you need to put the value where you want to set the props as '{{whatever}}'.
* Then when supplying the propsObject to the setProps function, you set the value by { whatever: your-value }
* @param {Object} propsObject
* @example
* const task = generator.createTree(`
* article className: 'task' id: '{{id}}'
* h2 className: 'task__title' innerText: '{{title}}'
* p className: 'task__description' innerText: '{{description}}'
* button className: 'task__remove-button' innerText: 'X'
* end`)
*
* task.setProps({
* id: 1,
* title: 'Buy Milk',
* description: 'With chocolate taste',
*})
*
* @returns {void}
*/
setProps(propsObject) {
for (const key in propsObject) {
const toFind = `{{${key}}}`;
this.#arrayOfElements.forEach(elem => {
for (const prop in elem.node) {
if (elem.node[prop] === toFind) {
elem.node[prop] = propsObject[key];
elem.props[prop] = propsObject[key];
}
}
})
}
}
/**
* @desc
* A clever way to set state directly to elements properties using Generator.
* It works similarily to setProps but with some modifications.
* To fully understand how this function works, it's recommended to read the docs about State and Generator first.
* When generating the component like setProps, you need to put the value where you want to set the state as '{{workerName.whateverProp}}' note the DOT '.'.
*
* When supplying the initial state to State,
* you should supply it as an object with the key name of the worker with the preferred value, f.e { whateverWorker: { whateverField: 'whateverValue' }} (See example below for clarification).
* The field is the name you supply when generating the elements (Check generator example below for clarification).
*
* The worker needs to return an object with all fields specified in the generator, else it will replace the state with undefined.
* Everytime the worker returns the object with the specified field, it will automatically update the value you supplied in the generator.
*
* This method is very useful when you want the state to be managed by the library instead of supplying custom functions to update the text.
*
* NOTE: The following example below is not using the intended project structure used for state, and should preferable be splitted into different files,
* but it's just a simple demonstration of how setState works.
* @example
* const whateverWorker = (state, action) => {
* switch (action.type) {
* case 'WHATEVER_ACTION':
* return { whateverText: state[action.field] + action.appendText };
* default:
* return state;
* }
* };
*
* const initState = { whateverWorker: { whateverText: 'yo' } };
* const workers = State.mergeWorkers({ whateverWorker });
* const state = new State(workers, initState);
* state.createAction('whateverAction', { type: 'WHATEVER_ACTION' });
*
* const generator = new Generator();
* const header = generator.createTree(`
* header
* div
* h1 innerText: '{{whateverWorker.whateverText}}'
* end`);
*
* header.setState(state);
* state.subscribe(header);
*
* //Gets the div as in the order supplied to generator
* header.elements[1].addEventListener('click', () => {
* state.dispatch(state.getAction('whateverAction', { appendText: 'HELLO', field: 'whateverText' }));
* })
*
* header.render();
* @param {Object} state
* @returns {void}
*/
setState(state) {
const statesValues = state.getState();
for (const key in statesValues) {
const toFind = `{{${key.replace(/\..+/gi, '')}}}`;
this.#arrayOfElements.forEach(elem => {
for (const prop in elem.node) {
if (String(elem.node[prop]).replace(/\.[a-z]+/gi, '') === toFind) {
let properties = elem.node[prop].match(/(?<=\.)[a-z]+/gi)[0] || null;
const updateState = () => {
if (properties) {
const stateObject = state.getState()[key];
elem.node[prop] = stateObject[properties];
}
else
elem.node[prop] = state.getState()[key];
}
updateState();
this.#stateUpdaterArray.push(updateState);
}
}
})
}
}
updateState() {
this.#stateUpdaterArray.forEach(func => {
func();
})
}
/**
* @desc
* A fluid function that returns the elements searched for in a component based on id, class or tag.
* It checks for # to find a id and in taht case returns the element directly.
* For tags and classes, it will always return an array of the found elements.
* @example
* const header = generator.createTree(`
* div id: 'hello'
* h1 innerText: 'Yo!'
* div
* h2 innerText: 'Welcome.'
* end`)
*
* const divWithIdHello = header.retrieve('#hello')
* const bothDivs = header.retrieve('div');
*
* @param {String} input
* @returns {Element}
*/
retrieve(input) {
let retrievedElements = [];
if (input[0] === '#') {
retrievedElements = this.#arrayOfElements
.find(element => element.node.id === input.replace('#', ''));
} else if (input[0] === '.') {
this.#arrayOfElements.forEach(element => {
if (element.node.className === input.replace('.', '')) {
retrievedElements.push(element);
}
})
} else {
this.#arrayOfElements.forEach(element => {
if (element.type === input) {
retrievedElements.push(element);
}
})
}
return retrievedElements;
}
/**
* @desc
* Changes the components grandparent to another element supplied.
*
* @example
* const generator = new Generator();
*
* const aNewParent = new Element('article', root, {}, true);
*
* const header = generator.createTree(`
* div id: 'grandparent'
* h1 innerText: 'Welcome!'
* div
* h2 innerText: 'To my page...'
* div
* div
* end`)
*
* header.changeParent(aNewParent);
* header.render();
* @param {Element} newParent
*/
changeParent(newParent) {
this.#parent = newParent
this.#arrayOfElements[0].changeParent(newParent.node);
}
sortInDom(prop = 'id', order = 'ascending') {
// Sorts wrappers in groups based on
// f.e ID in the DOM elems and just use insertbefores to
// change only those that needs to change.
// You can also add so it regexes away for example "task1" so it just looks for the numbers.
/**
* Algoritmen:
* Nested Loop with one element,
* insertBefore on the first element that has the same or higher value than element
*
*/
}
/**
* @desc
* Calls node.appendChild for every node inside the elements of the component.
*/
render() {
this.#arrayOfElements.forEach(element => {
element.addNode();
})
}
/**
* @desc
* Calls node.removeChild for every node inside the elements of the component.
*/
unrender() {
this.#arrayOfElements.forEach(element => {
if (!element.removed)
element.removeNode();
})
}
/**
* @desc
* Discards element inside component based on index, either from array supplied or the order from generator.createTree.
* Calling this before render has undefined behavior.
* Note that index 0 will remove the whole component.
* @param {Number} index
*/
deleteByIndex(index) {
const targetElement = this.#arrayOfElements[index]
if (!targetElement.removed)
targetElement.removeNode();
this.#arrayOfElements.splice(index, 1);
}
/**
* @desc
* Deletes element based on ID, doesn't need any #.
* Else the same applies to this function as deleteByIndex.
* @param {String} id
*/
deleteById(id) {
const index = this.#arrayOfElements.findIndex(elem => elem.id === id);
const targetElement = this.#arrayOfElements[index];
if (!targetElement.removed)
targetElement.removeNode();
this.#arrayOfElements.splice(index, 1);
}
}