Passing element to composable function in Vue composition API? - javascript

I'll preface this question by saying I'm very new to the composition API so I'm still trying to figure out the best practices. I'm writing a useResizeObserver composable function using Vue's composition API in order to track dimension changes on a target element. Because I have to wait for the element to be mounted, I can't call my composable from top-level in the setup() method on my component as most examples show, but have to call it from onMounted() instead, where I have access to my element's ref.
onMounted(async () => {
elementDimensions.value = useResizeObserver(modalContent.value);
}
This forced me to have another ref that I named elementDimensions actually declared at the top level of the setup() function, which I initialise to null.
setup(props) {
const elementDimensions = ref(null);
const modalContent = ref(null);
...
}
Here is the useResizeObserver function itself:
export default function useResizeObserver(element) {
const elementHeight = ref(0);
const elementWidth = ref(0);
const resizeObserver = new ResizeObserver(entries => {
elementHeight.value = entries[0].contentRect.height;
elementWidth.value = entries[0].contentRect.width;
});
if (element) {
resizeObserver.observe(element);
}
onUnmounted(() => {
resizeObserver.disconnect();
});
return { elementHeight, elementWidth };
}
This actually works well to give me what I want but I'm wondering if there's a better way to achieve this. The way things are implemented right now, I end up with "nested" refs, since I end up wrapping elementHeight and elementWidth (which are already refs) into another ref inside the component (elementDimensions). Is there a better recommended way of doing this when you need to pass an element to a composable from onMounted()?

Composition functions are supposed to be used directly in setup, any other uses depend on the implementation and need to be verified.
Refs are basically objects that allow to pass a value by reference rather than by value. One of their uses is to pass a ref early and access when a value is up-to-date.
It should be:
setup(props) {
const modalContent = ref(null);
const elementDimensions = useResizeObserver(modalContent);
...
}
and
export default function useResizeObserver(element) {
const elementHeight = ref(0);
const elementWidth = ref(0);
let resizeObserver;
onMounted(() => {
if (element.value) {
resizeObserver = new ResizeObserver(...);
resizeObserver.observe(element.value);
}
}
onUnmounted(() => {
if (resizeObserver)
resizeObserver.disconnect();
});
return { elementHeight, elementWidth };
}

Related

Best practice for using scoped variables between imported functions

I am trying to create an easing function on scroll but my main function is growing rather large and I want to be able to split it up. I am creating a requestAnimationFrame function that will ease the page scroll. The big issue I am having is that the render function with the animation frame will ease the Y value and then calls the update function to update the elements. But if I split this up and import them individually I am having trouble figuring out how to pass the updated values between functions. Many other functions also rely on these updated values.
I could take an object oriented approach and create a class or a constructor function and bind this to the function but it seems like bad practice to me:
import render from './render'
import update from './update'
const controller = () => {
this.items = [];
this.event = {
delta: 0,
y: 0
}
this.render = render.bind(this)
this.update = update.bind(this)
//ect
}
I could also break it up into classes that extend each other but I would like to take a more functional approach to the situation but I am having trouble figuring out how to achieve this.
Here is a very condensed version of what I am trying to do. Codesandbox.
const controller = (container) => {
const items = [];
let aF = null;
const event = {
delta: 0,
y: 0
};
const render = () => {
const diff = event.delta - event.y;
if (Math.abs(diff) > 0.1) {
event.y = lerp(event.y, event.delta, 0.06);
aF = requestAnimationFrame(render);
} else {
stop();
}
update();
};
const start = () => {
if (!aF) aF = requestAnimationFrame(render);
};
const stop = () => {
event.y = event.delta;
cancelAnimationFrame(aF);
aF = null;
};
const update = () => {
const y = event.y;
container.style.transform = `translateY(-${y}px)`;
items.forEach((item) => {
item.style.transform = `translate(${y}px, ${y}px)`;
});
};
const addItem = (item) => items.push(item);
const removeItem = (item) => {
const idx = items.indexOf(item);
if (idx > -1) items.splice(idx, 1);
};
const onScroll = () => {
event.delta = window.scrollY;
start();
};
// and a bunch more stuff
window.addEventListener("scroll", onScroll);
return {
addItem,
removeItem
};
};
export default controller;
I would like to be able to split this up and create a more functional approach with pure functions where I can import the update and render functions. The problem is that these functions are reliant on the updated global variables. Everywhere I look it says that global variables are a sin but I don't see a way to avoid them here. I have tried to look at the source code for some large frameworks to get some insight on how they structure their projects but it is too much for me to take in at the moment.
Any Help would be appreciated. Thanks.
What you wrote in the first example is completely valid, you basically did what classes are doing under the hood (classes are just syntactic sugar almost), but there are some problems with your code:
You use an arrow function, so this becomes window actually in that context.
You should use function expression instead, and initialize your instance using new, only in this case this will work how you want.
However, there are many solutions for Dependency Injection / Plugin systems which is what you're basically looking for, I'd advise you to look around this area.
In case you want a more functional approach, you need to avoid the this keyword and you need to use pure functions. I'd advice you not to share values in a context, but simply pass the necessary dependencies to your functions.
import render from './render'
const controller = () => {
const items = []
window.addEventListener('scroll', (event) => {
start({ y: event.y })
})
const start = ({ y }) => {
// Pass what render needs
requestAnimationFrame(() => render({ container, items, y }))
}
}
Using the above:
You don't need the update function in this file, render will import it on its own.
Your functions are pure, easily testable on their own.
You don't need to create an instance by using new (it's actually more expensive).
No need for shared state across functions.
No DI/Plugin solution need to implemented/used.
In case you do want to have shared state, you won't do that without classes and/or extra tooling around.
The way I'd do this is by creating a seperate .js file, then send the information you need into that js file right at the start of your main files code. Then just use the seperate file to store all your functions, then just call them with utils.myFunction(). At least that's how I do it in node.

React useEffect in function

I've been trying to find a way to use useEffect like this, but I don't think I'm doing it properly:
const [searching, setSearching] = useState(false);
function OnSearch(e) {
const searchValue = e.target.value;
useEffect(() => {
setSearching(true);
}, []);
clearTimeout(timer);
timer = setTimeout(() => {
setSearching(false);
window.location.href = '/block/' + searchValue;
}, 2000);
}
Any kind of help or direction will be appreciated.
I assume from the useState you've used there that this code is inside a functional component. You don't need or want useEffect there, just remove it and do setSearching(true) directly:
const [searching, setSearching] = useState(false);
function OnSearch(e) {
const searchValue = e.target.value;
setSearching(true);
clearTimeout(timer);
timer = setTimeout(() => {
setSearching(false);
window.location.href = '/block/' + searchValue;
}, 2000);
}
(In fact, not only do you not need it, but you can't use hooks anywhere other than the top level of a component function or hook function. So not in an event handler.)
That leaves the question of the timer variable. It can't just be a local variable in the component function because that will get recreated on every render. For non-state instance data like that, you can use an object via useRef, then use properties on that object, which will be consistent for the lifetime of the component:
const [instance] = useRef({});
So that gives us:
const [instance] = useRef({});
const [searching, setSearching] = useState(false);
function OnSearch(e) {
const searchValue = e.target.value;
setSearching(true);
clearTimeout(instance.timer);
instance.timer = setTimeout(() => {
setSearching(false);
window.location.href = '/block/' + searchValue;
}, 2000);
}
Side note: In JavaScript, the overwhelmingly-common convention is that only constructor functions and (in React) component functions are capitalized, not other kinds of functions. So onSearch rather than OnSearch. You can do what you like in your own code of course, but when working with others or asking for help, sticking to conventions helps keep communication clear.

Is there anyway to set a variable equal to function that is called everytime the variable is accessed

I am trying to re-create the 'useState' hook from React as a silly fun personal exercise, but am encountering trouble when accessing variables from my 'global state'. I know that this has no practical use outside of react, but I just thought it would be something to attempt regardless.
Currently I have the following implementation below, but because the destructured variable is set only that first time and is not updated when using the corresponding setter, it always will return the same variable. I fully understand why this is happening but I am unsure if there is a way to get this working at all or if this is a lost cause. The destructured setter does update the global state, but the variable is of course as previously mentioned not accessing the global state again since it is only set that initial time.
let PROP_ID = 0;
let GLOBAL_STATE = {};
const useState = prop => {
const id = PROP_ID++;
GLOBAL_STATE[id] = prop;
return [
(() => {
return GLOBAL_STATE[id];
})(),
function(nv) {
GLOBAL_STATE[id] = nv;
}
];
};
const [userName, setUserName] = useState("Chris");
const [favCol, setFavCol] = useState("red");
console.log(GLOBAL_STATE);
console.log(userName);
setUserName("Bob");
console.dir(GLOBAL_STATE);
console.log(userName);
All I want to know if there is a way to set the destructured reference variable equal to some sort of function that will always be called to get the new variable from the global state when that variable is referenced.
I think you're missing a piece of the puzzle here.
React hooks depend on the position of their call within a given functional component. Without the encapsulating function, you remove the usefulness of the state being provided by the hook, because they're only being called once in your example, thus the reference in the destructuring syntax never gets updated as you observed.
Let's get them working in the context of functions.
const { component, useState } = (function () {
const functions = new WeakMap()
const stack = []
let hooks
let index
function component (fn) {
return function (...args) {
try {
stack.push({ hooks, index })
hooks = functions.get(fn)
index = 0
if (!hooks) {
functions.set(fn, hooks = [])
}
return fn.apply(this, args)
} finally {
({ hooks, index } = stack.pop())
}
}
}
function useState (initialValue) {
const hook = index++
if (hook === hooks.length) {
hooks.push(initialValue)
}
return [
hooks[hook],
function setState (action) {
if (typeof action === 'function') {
hooks[hook] = action(hooks[hook])
} else {
hooks[hook] = action
}
}
]
}
return { component, useState }
})()
const fibonacci = component(function () {
const [a, setA] = useState(1)
const [b, setB] = useState(1)
setA(b)
setB(a + b)
return a
})
const sequence = component(function () {
const [text, setText] = useState('')
setText(
text.length === 0
? fibonacci().toString()
: [text, fibonacci()].join()
)
return text
})
for (let i = 0; i < 20; i++) {
console.log(sequence())
}
The stack variable here allows us to nest our stateful function calls, and the hooks variable keeps track of the existing hook states by position within the currently executing component of the stack.
This implementation might seem overly-complicated, but the point of component() and stack is to partially mimic how the React framework treats functional components. This is still much simpler than how React works, because we're treating all calls of the same function as if it's the same instance of a functional component.
On the other hand, in React, a particular function could be used for several different instances, distinguishable from each other based on a number of factors such as the position in the hierarchy of the virtual DOM, the key and ref props, etc., so it's much more complicated than this.
It occurs to me you just want to get your example working. For that, all you need to do is change your variable to a getter function:
const useState = state => [
() => state,
value => { state = value }
];
const [getUserName, setUserName] = useState('Chris');
const [getFavCol, setFavCol] = useState('red');
console.log(getUserName());
setUserName('Bob');
console.log(getUserName());
Much simpler than what you had and doesn't require any globals to work.
If the manual getter seems too inconvenient, then you can't destructure, but you can implement an approach that's almost as easy to use:
const useState = state => ({
get state () { return state },
set (value) { state = value }
});
const userName = useState('Chris');
const favCol = useState('red');
console.log(userName.state);
userName.set('Bob');
console.log(userName.state);
This is a very interesting question.
The short answer is: no, that’s not possible*.
The long answer
The long answer is how JavaScript handles primitives and objects. Primitives values are copied during assignment (userName here: const [userName, setUserName] = useState("Chris"); which is a string), while in case of object a reference would be copied.
In order to play with it, I came with something like that (mind you, that is not solution to your challenge, rather explanation to my answer):
let PROP_ID = 0;
let GLOBAL_STATE = {};
const useState = prop => {
const id = PROP_ID++;
GLOBAL_STATE[id] = {
_value: prop,
[Symbol.toPrimitive]() { return this._value },
}
const tuple = [
GLOBAL_STATE[id],
(nv) => GLOBAL_STATE[id]._value = nv,
];
return tuple;
};
const [userName, setUserName] = useState("Chris");
const [favCol, setFavCol] = useState("red");
console.log(GLOBAL_STATE);
console.log(userName);
console.log('set user name to:', setUserName("Bob"));
console.dir(GLOBAL_STATE);
console.log('' + userName);
GLOBAL_STATE entry is now object, so when you destructure it after calling useState only a reference is changed. Then update changes data inside this object but what we assigned in the first place is still there.
I added Symbo.toPrimitive property which coerses object to a primitive value but sadly, this will not work on it’s own. Only when run as '' + userName. Which means it behaves differently than you expected. At this point I stopped experimenting.
React
I went to Facebook’s Github and tried to trace what they are doing but gave up due to imports of imports of imports. Hence, I will take an educated guess here, based on Hooks API behaviour. I think that your implementation is rather faithful to the original. We use useState in a function and the value doesn’t change there. Only when state is changed and then the component is re-rendered with a new value, which again is assigned and won’t change.
*I will gladly welcome anyone who proves this notion wrong.
How about something along the following lines...
let PROP_ID = 0;
let GLOBAL_STATE = {};
const useState = (varName, prop) => {
const id = PROP_ID++;
GLOBAL_STATE[id] = prop;
Object.defineProperty(window, varName, {
get: function(){
return GLOBAL_STATE[id];
}
});
return ((nv) => {
GLOBAL_STATE[id] = nv;
});
};
const setUserName = useState("userName", "Chris");
const setFavCol = useState("favCol", "red");
console.log(GLOBAL_STATE);
console.log(userName);
setUserName("Bob");
console.dir(GLOBAL_STATE);
console.log(userName);
Note that I've changed your interface a bit such that you have to pass the name of the variable to the useState function. Seems a bit kludgey, but allows a getter to be configured, in this case, following your example, on the global scope (ie, "window"), which might not be the best practice.
There is a solution, but only if you're okay using a dirty, dirty hack.
The following approach uses a with statement and a Proxy containing a custom get handler, and requires object destructuring syntax in order to determine the variable name from the property key of the setter function:
// initialize useState() hook with independent scope
function createHook (scope = Object.create(null)) {
const setter = /^set([A-Z][^\W_]*)$/;
function useState (initialValue) {
// return proxy from useState() so that object destructuring syntax
// can be used to get variable name and initialize setter function
return new Proxy(scope, {
get (target, propertyKey) {
if (!setter.test(propertyKey)) {
throw new TypeError(`Invalid setter name '${propertyKey}'`);
}
// get variable name from property key of setter function
const [, name] = propertyKey.match(setter);
const key = name[0].toLowerCase() + name.slice(1);
// support updater callback
const setState = value => {
target[key] = (
typeof value === 'function'
? value(target[key])
: value
);
};
// initialize state
setState(initialValue);
// return setter as property value
return setState;
}
});
}
return { scope, useState };
}
// example usage with a little magic
{
const { scope, useState } = createHook();
const { setFoo } = useState('bar');
console.log(scope.foo);
setFoo(42);
console.log(scope.foo);
}
// example use with more magic
const { scope, useState } = createHook();
with (scope) {
const { setUserName } = useState('Chris');
const { setFavCol } = useState('red');
console.log(userName, favCol);
setUserName('Bob');
setFavCol(color => `dark${color}`);
console.log(userName, favCol);
}
The following usage ends up being very similar to Jon Trent's answer by abusing implicit globals:
function createHook(e=Object.create(null)){var t=/^set([A-Z][^\W_]*)$/;return{scope:e,useState:n=>new Proxy(e,{get(e,r){if(!t.test(r))throw new TypeError(`Invalid setter name '${r}'`);var[,o]=r.match(t),c=o[0].toLowerCase()+o.slice(1),s=t=>{e[c]="function"==typeof t?t(e[c]):t};return s(n),s}})}}
const { useState } = createHook(window);
const { setUserName } = useState('Chris');
const { setFavCol } = useState('red');
console.log(userName, favCol);
setUserName('Bob');
setFavCol(color => `dark${color}`);
console.log(userName, favCol);

How does React.useState triggers re-render?

import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
In the above example whenever setCount(count + 1) is invoked a re-render happens. I am curious to learn the flow.
I tried looking into the source code. I could not find any reference of useState or other hooks at github.com/facebook/react.
I installed react#next via npm i react#next and found the following at node_modules/react/cjs/react.development.js
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
On tracing back for dispatcher.useState(), I could only find the following ...
function resolveDispatcher() {
var dispatcher = ReactCurrentOwner.currentDispatcher;
!(dispatcher !== null) ? invariant(false, 'Hooks can only be called inside the body of a function component.') : void 0;
return dispatcher;
}
var ReactCurrentOwner = {
/**
* #internal
* #type {ReactComponent}
*/
current: null,
currentDispatcher: null
};
I wonder where can I find dispatcher.useState() implementation and learn how it triggers re-render when setState setCount is invoked.
Any pointer would be helpful.
Thanks!
The key in understanding this is the following paragraph from the Hooks FAQ
How does React associate Hook calls with components?
React keeps track of the currently rendering component. Thanks to the Rules of Hooks, we know that Hooks are only called from React components (or custom Hooks — which are also only called from React components).
There is an internal list of “memory cells” associated with each component. They’re just JavaScript objects where we can put some data. When you call a Hook like useState(), it reads the current cell (or initializes it during the first render), and then moves the pointer to the next one. This is how multiple useState() calls each get independent local state.
(This also explains the Rules of Hooks. Hooks need to be called unconditionally in the same order, otherwise the association of memory cell and hook is messed up.)
Let's walk through your counter example, and see what happens. For simplicity I will refer to the compiled development React source code and React DOM source code, both version 16.13.1.
The example starts when the component mounts and useState() (defined on line 1581) is called for the first time.
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
As you have noticed, this calls resolveDispatcher() (defined on line 1546). The dispatcher refers internally to the component that's currently being rendered. Within a component you can (if you dare to get fired), have a look at the dispatcher, e.g. via
console.log(React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher.current)
If you apply this in case of the counter example, you will notice that the dispatcher.useState() refers to the react-dom code. When the component is first mounted, useState refers to the one defined on line 15986 which calls mountState(). Upon re-rendering, the dispatcher has changed and the function useState() on line 16077 is triggered, which calls updateState(). Both methods, mountState() on line 15352 and updateState() on line 15371, return the count, setCount pair.
Tracing ReactCurrentDispatcher gets quite messy. However, the fact of its existence is already enough to understand how the re-rendering happens. The magic happens behind the scene. As the FAQ states, React keeps track of the currently rendered component. This means, useState() knows which component it is attached to, how to find the state information and how to trigger the re-rendering.
setState is a method on the Component/PureComponent class, so it will do whatever is implemented in the Component class (including calling the render method).
setState offloads the state update to enqueueSetState so the fact that it's bound to this is really only a consequence of using classes and extending from Component. Once, you realize that the state update isn't actually being handled by the component itself and the this is just a convenient way to access the state update functionality, then useState not being explicitly bound to your component makes much more sense.
I also tried to understand the logic behind useState in a very simplified and basic manner, if we just look into its basic functionalities, excluding optimizations and async behavior, then we found that it is basically doing 4 things in common,
maintaining of State, primary work to do
re-rendering of the component through which it get called so that caller component can get the latest value for state
as it caused the re-rendering of the caller component it means it must maintain the instance or context of that component too, which also allows us to use useState for multiple component at once.
as we are free to use as many useState as we want inside our component that means it must maintain some identity for each useState inside the same component.
keeping these things in mind I come up with the below snippet
const Demo = (function React() {
let workInProgress = false;
let context = null;
const internalRendering = (callingContext) => {
context = callingContext;
context();
};
const intialRender = (component) => {
context = component;
workInProgress = true;
context.state = [];
context.TotalcallerId = -1; // to store the count of total number of useState within a component
context.count = -1; // counter to keep track of useStates within component
internalRendering(context);
workInProgress = false;
context.TotalcallerId = context.count;
context = null;
};
const useState = (initState) => {
if (!context) throw new Error("Can only be called inside function");
// resetting the count so that it can maintain the order of useState being called
context.count =
context.count === context.TotalcallerId ? -1 : context.count;
let callId = ++context.count;
// will only initialize the value of setState on initial render
const setState =
!workInProgress ||
(() => {
const instanceCallerId = callId;
const memoizedContext = context;
return (updatedState) => {
memoizedContext.state[instanceCallerId].value = updatedState;
internalRendering(memoizedContext);
};
})();
context.state[callId] = context.state[callId] || {
value: initState,
setValue: setState,
};
return [context.state[callId].value, context.state[callId].setValue];
};
return { useState, intialRender };
})();
const { useState, intialRender } = Demo;
const Component = () => {
const [count, setCount] = useState(1);
const [greeting, setGreeting] = useState("hello");
const changeCount = () => setCount(100);
const changeGreeting = () => setGreeting("hi");
setTimeout(() => {
changeCount();
changeGreeting();
}, 5000);
return console.log(`count ${count} name ${greeting}`);
};
const anotherComponent = () => {
const [count, setCount] = useState(50);
const [value, setValue] = useState("World");
const changeCount = () => setCount(500);
const changeValue = () => setValue("React");
setTimeout(() => {
changeCount();
changeValue();
}, 10000);
return console.log(`count ${count} name ${value}`);
};
intialRender(Component);
intialRender(anotherComponent);
here useState and initialRender are taken from Demo. intialRender is use to call the components initially, it will initialize the context first and then on that context set the state as an empty array (there are multiple useState on each component so we need array to maintain it) and also we need counter to make count for each useState, and TotalCounter to store total number of useState being called for each component.
FunctionComponent is different. In the past, they are pure, simple. But now they have their own state.
It's easy to forget that react use createElement wrap all the JSX node, also includes FunctionComponent.
function FunctionComponent(){
return <div>123</div>;
}
const a=<FunctionComponent/>
//after babel transform
function FunctionComponent() {
return React.createElement("div", null, "123");
}
var a = React.createElement(FunctionComponent, null);
The FunctionComponent was passed to react. When setState is called, it's easy to re-render;

Basic ES6 Javascript Plugin - reuse variable between functions

I'm attempting to build a basic JS plugin that can be called after a click event to disable a button (to prevent users firing multiple API calls) and to give feedback that something is loading/happening. Here is how it looks:
This works great on an individual basis, but I want to re-write it as a plugin so I can reuse it across the site.
Here is a cut down version of the JS from file loader.plugin.js.
let originalBtnText;
export function showBtnLoader(btn, loadingText) {
const clickedBtn = btn;
const spinner = document.createElement('div');
spinner.classList.add('spin-loader');
originalBtnText = clickedBtn.textContent;
clickedBtn.textContent = loadingText;
clickedBtn.appendChild(spinner);
clickedBtn.setAttribute('disabled', true);
clickedBtn.classList.add('loading');
return this;
}
export function hideBtnLoader(btn) {
const clickedBtn = btn.target;
clickedBtn.textContent = originalBtnText;
clickedBtn.removeAttribute('disabled');
clickedBtn.classList.remove('loading');
return this;
}
export function btnLoader() {
showBtnLoader();
hideBtnLoader();
}
And here is an example of how I would like to use it.
import btnLoader from 'loaderPlugin';
const signupBtn = document.getElementById('signup-btn');
signupBtn.addEventListener('click', function(e) {
e.preventDefault();
btnLoader.showBtnLoader(signupBtn, 'Validating');
// Call API here
});
// Following API response
hideBtnLoader(signupBtn);
The issue I have is that I want to store the originalBtnText from the showBtnLoader function and then use that variable in the hideBtnLoader function. I could of course achieve this in a different way (such as adding the value as a data attribute and grabbing it later) but I wondered if there is a simple way.
Another issue I have is that I don't know the correct way of calling each individual function and whether I am importing it correctly. I have tried the following.
btnLoader.showBtnLoader(signupBtn, 'Validating');
btnLoader(showBtnLoader(signupBtn, 'Validating'));
showBtnLoader(signupBtn, 'Validating');
But I get the following error:
Uncaught ReferenceError: showBtnLoader is not defined
at HTMLButtonElement.<anonymous>
I have read some good articles and SO answers such as http://2ality.com/2014/09/es6-modules-final.html and ES6 export default with multiple functions referring to each other but I'm slightly confused as to the 'correct' way of doing this to make it reusable.
Any pointers would be much appreciated.
I would export a function that creates an object with both show and hide functions, like this:
export default function(btn, loadingText) {
function show() {
const clickedBtn = btn;
const spinner = document.createElement('div');
spinner.classList.add('spin-loader');
originalBtnText = clickedBtn.textContent;
clickedBtn.textContent = loadingText;
clickedBtn.appendChild(spinner);
clickedBtn.setAttribute('disabled', true);
clickedBtn.classList.add('loading');
}
function hide() {
const clickedBtn = btn.target;
clickedBtn.textContent = originalBtnText;
clickedBtn.removeAttribute('disabled');
clickedBtn.classList.remove('loading');
}
return {
show,
hide,
};
}
Then, to use it:
import btnLoader from 'btnloader';
const signupBtn = document.getElementById('signup-btn');
const signupLoader = btnLoader( signupBtn, 'Validating' );
signupBtn.addEventListener('click', function(e) {
e.preventDefault();
signupLoader.show();
// Call API here
});
// Following API response
signupLoader.hide();
If you need to hide it from a different file from where you showed it, then you can export the instance:
export const signupLoader = btnLoader( signupBtn, 'Validating' );
And later import it.
import { signupLoader } from 'signupform';
function handleApi() {
signupLoader.hide();
}
Youre maybe overriding the Element.prototype, to make it accessible right from that element. However, i wouldnt set values onto that element, i would rather return an object with all the neccessary stuff:
export function implementBtnLoader(){
Element.prototype.showBtnLoader=function( loadingText) {
const clickedBtn = this;
const spinner = document.createElement('div');
spinner.classList.add('spin-loader');
var originalBtnText = clickedBtn.textContent;
clickedBtn.textContent = loadingText;
clickedBtn.appendChild(spinner);
clickedBtn.setAttribute('disabled', true);
clickedBtn.classList.add('loading');
return {
text:originalBtnText,
el:this,
hideBtnLoader: function() {
const clickedBtn = this.target;
clickedBtn.textContent = this.text;
clickedBtn.removeAttribute('disabled');
clickedBtn.classList.remove('loading');
return this;
}
};
};
}
export function btnLoader() {
implementBtnLoader();
}
When imported, and implementBtnLoader was called, one can do:
var loader=document.getElementById("test").showBtnLoader();
console.log(loader.text);
loader.hideBtnLoader();

Categories

Resources