tutorial // Aug 19, 2022
How to Dynamically Position Elements in the DOM with JavaScript
How to use JavaScript to dynamically manipulate DOM elements relative to other DOM elements.
For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.
To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):
Terminal
npm i -g @joystick.js/cli
This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:
Terminal
joystick create app
After a few seconds, you will see a message logged out to cd
into your new project and run joystick start
:
Terminal
cd app && joystick start
After this, your app should be running and we're ready to get started.
Why?
At first glance, this may seem a bit silly. Why would we want to do this? Well, when you start to build more complex interfaces, though many UI patterns are best attempted through CSS first, sometimes, that makes things more complicated than necessary. When that's the case for your own app, it's good to know how to apply styles via JavaScript to handle changes in your UI to avoid messy or fragile CSS.
Setting up our test case
For this tutorial, we're going to work with a Joystick component. This is the UI half of the Joystick framework we just set up. This will allow us to build out a UI quickly using plain HTML, CSS, and JavaScript.
To start, in the app that was created for us when we ran joystick create app
, open up the /ui/pages/index/index.js
file. Once you've got it, replace the contents with the following:
/ui/pages/index/index.js
import ui from '@joystick.js/ui';
const Index = ui.component({
render: () => {
return `
<div>
</div>
`;
},
});
export default Index;
Here, we're replacing the existing example component that's mapped to the root route in our application http://localhost:2600/
(or just /
) with a skeleton component that we can use to build out our test case.
Next, let's replace that <div></div>
being returned by the render()
method (this is the HTML that will be rendered or "drawn" on screen) with a list of "cards" that we'll dynamically position later with JavaScript:
/ui/pages/index/index.js
import ui from '@joystick.js/ui';
const Index = ui.component({
render: () => {
return `
<div class="index">
<ul class="cards">
<li>
<h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
<p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
</li>
<li>
<h2>Ab recusandae minima commodi sed pariatur.</h2>
<p>Velit in voluptatum quia consequatur fuga et repellendus ut cupiditate. Repudiandae dignissimos dolores qui. Possimus nihil laboriosam enim dolorem vitae accusantium accusamus dolor. Tenetur fuga omnis et est accusantium dolores. Possimus vitae aliquid. Vitae commodi et autem vitae rerum.</p>
</li>
<li>
<h2>Voluptatem ipsa sed illum numquam aliquam sint.</h2>
<p>Suscipit quis error dolorum sed recusandae recusandae est. Et tenetur perferendis sequi itaque similique. Porro facere qui saepe alias. Qui itaque corporis explicabo itaque. Quibusdam vel expedita odio quaerat libero veniam praesentium minus.</p>
</li>
<li>
<h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
<p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
</li>
<li>
<h2>Ab recusandae minima commodi sed pariatur.</h2>
<p>Velit in voluptatum quia consequatur fuga et repellendus ut cupiditate. Repudiandae dignissimos dolores qui. Possimus nihil laboriosam enim dolorem vitae accusantium accusamus dolor. Tenetur fuga omnis et est accusantium dolores. Possimus vitae aliquid. Vitae commodi et autem vitae rerum.</p>
</li>
<li>
<h2>Voluptatem ipsa sed illum numquam aliquam sint.</h2>
<p>Suscipit quis error dolorum sed recusandae recusandae est. Et tenetur perferendis sequi itaque similique. Porro facere qui saepe alias. Qui itaque corporis explicabo itaque. Quibusdam vel expedita odio quaerat libero veniam praesentium minus.</p>
</li>
<li>
<h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
<p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
</li>
<li>
<h2>Ab recusandae minima commodi sed pariatur.</h2>
<p>Velit in voluptatum quia consequatur fuga et repellendus ut cupiditate. Repudiandae dignissimos dolores qui. Possimus nihil laboriosam enim dolorem vitae accusantium accusamus dolor. Tenetur fuga omnis et est accusantium dolores. Possimus vitae aliquid. Vitae commodi et autem vitae rerum.</p>
</li>
<li>
<h2>Voluptatem ipsa sed illum numquam aliquam sint.</h2>
<p>Suscipit quis error dolorum sed recusandae recusandae est. Et tenetur perferendis sequi itaque similique. Porro facere qui saepe alias. Qui itaque corporis explicabo itaque. Quibusdam vel expedita odio quaerat libero veniam praesentium minus.</p>
</li>
<li>
<h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
<p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
</li>
<li>
<h2>Ab recusandae minima commodi sed pariatur.</h2>
<p>Velit in voluptatum quia consequatur fuga et repellendus ut cupiditate. Repudiandae dignissimos dolores qui. Possimus nihil laboriosam enim dolorem vitae accusantium accusamus dolor. Tenetur fuga omnis et est accusantium dolores. Possimus vitae aliquid. Vitae commodi et autem vitae rerum.</p>
</li>
<li>
<h2>Voluptatem ipsa sed illum numquam aliquam sint.</h2>
<p>Suscipit quis error dolorum sed recusandae recusandae est. Et tenetur perferendis sequi itaque similique. Porro facere qui saepe alias. Qui itaque corporis explicabo itaque. Quibusdam vel expedita odio quaerat libero veniam praesentium minus.</p>
</li>
</ul>
</div>
`;
},
});
export default Index;
Very simple. Here, we've added a class index
to the existing <div></div>
and inside, we've added a <ul></ul>
(unordered list) with a class cards
. Inside, we've added 12 <li></li>
tags, each representing a "card" with some lorem ipsum content on it. Though the length is technically arbitrary, in order to make sense of what we'll implement below, it makes sense to have several items as opposed to 1-2 (feel free to play with the length, though, as our code will still work).
/ui/pages/index/index.js
import ui from '@joystick.js/ui';
const Index = ui.component({
css: `
.cards {
opacity: 0;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
padding: 40px;
overflow-x: scroll;
display: flex;
}
.cards li {
background: #fff;
border: 1px solid #eee;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
padding: 30px;
border-radius: 3px;
list-style: none;
width: 300px;
min-width: 300px;
}
.cards li h2 {
font-size: 28px;
line-height: 36px;
margin: 0;
}
.cards li p {
font-size: 16px;
line-height: 24px;
color: #888;
}
.cards li:not(:last-child) {
margin-right: 30px;
}
`,
render: () => {
return `
<div class="index">
<ul class="cards">
<li>
<h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
<p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
</li>
...
</ul>
</div>
`;
},
});
export default Index;
Just above our render
method, we've added a property to our component css
which, as you'd expect, allows us to add some CSS styling to our component. What these styles are achieving is to give us a horizontally scrolled list of "cards" that extend past the edge of the browser, like this:
Now that we have our base styles and markup in the browser, next, we want to add the JavaScript necessary to dynamically shift the first card in the list to start at the middle of the page. Our goal is to mimic a design like the "what's new" list on the current Apple Store design:
To do it, next, we're going to wire up the JavaScript necessary as a method on our Joystick component.
Dynamically setting padding on page load
Before we handle the "on page load" part here, first, we need to write the JavaScript to select our list in the DOM, calculate the current center point of the window, and then set the left-side padding of our list. Here's how we do it:
/ui/pages/index/index.js
import ui from '@joystick.js/ui';
const Index = ui.component({
state: {
defaultListPadding: '20px',
},
methods: {
handleSetListPadding: (component = {}) => {
const list = component.DOMNode.querySelector('ul.cards');
const windowCenterPoint = window.innerWidth / 2;
if (list) {
list.style.paddingLeft = windowCenterPoint >= 400 ? `${windowCenterPoint}px` : component.state.defaultListPadding;
list.style.opacity = 1;
}
},
},
css: `...`,
render: () => {
return `
<div class="index">
<ul class="cards">
<li>
<h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
<p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
</li>
...
</ul>
</div>
`;
},
});
export default Index;
On a Joystick component, a "method" (defined as a method function on the methods
property of our component's option) is a miscellaneous function on our component that can be called from anywhere in the component. Here, we've defined handleSetListPadding
as a method so that we can call it when our component mounts on screen (more on this in a bit).
To start, we add an argument as component
which is automatically handed to us by Joystick (the framework automatically assigns the last possible argument on a function to be the component instance—since we don't have any arguments, it defaults to the first slot). On that component
instance object, we're given a DOMNode
property which represents the rendered DOM node for our component (in this case the Index
component we're authoring) in the browser.
From that, we can use vanilla JavaScript DOM selection and here, we do that by using the .querySelector()
method on that DOM node to locate our ul.cards
list, storing it in a variable list
.
Next, because we want to set that list's left-side padding to be the center of the window, we need to calculate what the pixel value of that center point is. To do it, we can take the window.innerWidth
value and divide it by 2
(for example, if our window is currently 1000
pixels wide, windowCenterPoint
would become 500
).
With our list
and windowCenterPoint
assuming we did find a list
element in the page, we want to modify the list.style.paddingLeft
value, setting it equal to a string value, concatenating the value of windowCenterPoint
with px
(we do this because the value we get is an integer but we need to set our padding as a pixel value).
Notice that here, we make this paddingLeft
value conditional based on the value of windowCenterPoint
. If the value is greater than 400
, we want to set it as the paddingLeft
. If it's not, we want to fall back to a default padding value (this ensures we don't accidentally shove the cards completely off screen for smaller viewports). To store this default, we've added the state
property to our component's options which is an object containing default values for the state of our component. Here, we've assigned defaultListPadding
to a string '20px'
which we use as the "else" in our windowCenterPoint >= 400
ternary.
Next, just beneath our call to set list.style.paddingLeft
we also make sure to set list.style.opacity
to 1. Why? Well, in our css
that we set earlier, we set our list to opacity: 0;
by default. This is a "trick" to prevent our list from jumping visually on page during a slow page load (hit or miss depending on connection speed). This removes any potential for a visual glitch which would be jarring to the user.
While we've got our code written, this currently won't do anything. To make it work, we need to actually call our method.
Calling handleSetListPadding on mount and window resize
This part is pretty simple, here's the code to get it done:
/ui/pages/index/index.js
import ui from '@joystick.js/ui';
const Index = ui.component({
state: {
defaultListPadding: '20px',
},
lifecycle: {
onMount: (component = {}) => {
component.methods.handleSetListPadding();
window.addEventListener('resize', () => {
component.methods.handleSetListPadding();
});
},
},
methods: {
handleSetListPadding: (component = {}) => {
const list = component.DOMNode.querySelector('ul.cards');
const windowCenterPoint = window.innerWidth / 2;
if (list) {
list.style.paddingLeft = windowCenterPoint >= 400 ? `${windowCenterPoint}px` : component.state.defaultListPadding;
list.style.opacity = 1;
}
},
},
css: `...`,
render: () => {
return `
<div class="index">
<ul class="cards">
<li>
<h2>Aliquam impedit ipsa adipisci et quae repellat sit.</h2>
<p>Deleniti quibusdam quia assumenda omnis. Rerum cum et error vero enim ex. Sapiente est est ut omnis possimus temporibus in.</p>
</li>
...
</ul>
</div>
`;
},
});
export default Index;
Adding one more option to our component lifecycle
, on the object passed to it we assign a property onMount
which is set to a function Joystick will call as soon as our component's HTML is rendered to the browser. Just like with our handleSetListPadding
method, Joystick automatically passes the component
instance to all of the available lifecycle methods.
Here, we use that component
instance to access our handleSetListPadding
method, calling it with component.methods.handleSetListPadding()
. In addition to this, we need to also consider the user resizing the browser and how this will affect the window's center point. All we need to do is add an event listener on the window
for the resize
event and in the callback that's called when that event is detected, another call to component.methods.handleSetListPadding()
.
This works because we're retrieving the value of window.innerWidth
at call time for the handleSetListPadding
function. Here, then, because we're getting that value after the resize has occurred, we can trust that window.innerWidth
will contain the current width and not the width that we had on page load.
That's it! Now if we load up our page in the browser, we should be able to resize and see our first card shift its left edge to align to the center of the window.
Wrapping up
In this tutorial, we learned how to manipulate the DOM dynamically with JavaScript. We learned how to dynamically position an element via its CSS using the DOM style
property on a list element. We also learned how to rely on the window
resize event to recalculate our browser's center point whenever the browser width changed.