内容简介:So, I've just unveiled my new open-source UI library calledAnyway, if you're interested in trying something new and fresh, maybe consider giving Isotope a try? You can go straight up to thedocs or bear with me, as we're going to make a simpleIsotope is wri
So, I've just unveiled my new open-source UI library called Isotope . It's fast, lightweight, modular and overall - I think it's pretty good.
Anyway, if you're interested in trying something new and fresh, maybe consider giving Isotope a try? You can go straight up to thedocs or bear with me, as we're going to make a simple TODO app , allowing us to learn the basics of Isotope.
Setup
Isotope is written in TypeScript that's transpiled down to pure JS, which requires no additional tooling to get you up & running.
To set up our project, we'll use npm
(but yarn
is also an option). We'll start by running run npm init
to create our base package.json
file. Then, install the Isotope and Bulma
- a CSS-only library that will make our app look slightly prettier!
npm install @isotope/core bulma
Now, you can use Isotope with any bundler you want (or go buildless ), but here, we'll use the Parcel - a zero-config bundler that doesn't require any setup whatsoever, and thus it's great for any kind of playground-like scenario!
npm install --dev parcel-bundler
With the bundler installed, we can start writing some code, or more specifically, the HTML!
<!DOCTYPE html> <html> <head> <title>Isotope Playground</title> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <script src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script> </head> <body> <script src="src/index.js"></script> </body> </html>
Aside from the basic boilerplate, we also load the Font Awesome
icon library through its CDN and include our main JS file, which is where the whole magic will happen. And that's it for HTML! Parcel will take care of the rest. Just make sure you've got all the files in correct places and run npx parcel index.html
to start the dev server
.
Container
So, now that we're all set up, let's get right into making our app. First, we have to create the a container that will house all our TODOs, as well as a form to add them.
import { createDOMView } from "@isotope/core"; import "bulma/css/bulma.min.css"; const view = createDOMView(document.body); const container = view .main({ classes: ["container", "fluid"], }) .div({ classes: ["columns", "is-centered", "is-vcentered", "is-mobile"], }) .div({ classes: ["column", "is-narrow"], styles: { width: "70%", }, });
In the snippet above we create our main container. We start by importing the createDOMView()
function from the Isotope library, which is responsible for creating aview - a top-level node, that attaches to the specified DOM element to render its content.
Here, we attach our view to the <body>
element, making Isotope effectively take control of the entire website. It's a nice solution for our current situation, but keep in mind that Isotope's progressive
nature, allows it to attach to any element to control even the smallest pieces of your UI.
So, we've got our top-level node attached to the <body>
element. This is a great start for our application. In Isotope anode is the most important entity and having access to even a single one, grants you the power to create more.
That's essentially what we do in the next line.
// ... const container = view.main({ classes: ["container", "fluid"], }); // ...
We use the view
reference we've got to create a new node - a child node that will append a new element to the DOM. For that, we use the main()
method - a method from the Isotope's HTML node pack.
Isotope'snode packs are essentially bundles of shortcut methods
that get applied directly to the node's prototype. main()
is one of such methods. It simplifies the creation of the <main>
element, which would otherwise require a bit longer syntax ( child("main")
).
To configure our new node, we have to use a configuration object. Here, we make use of the classes
config property, to add some CSS classes to the element.
So, to summarize, we create a new node which represents a <main>
element - child to <body>
- that has "container"
and "fluid"
CSS classes applied to it. On a side note - all of the used class names come from Bulma, which we import at the top of our JS file thanks to Parcel CSS imports support.
The main()
like all other methods from the HTML node pack, returns the newly-created node. In this way we get the ability to add new child nodes to this node, effectively building our UI.
const container = view .main({ classes: ["container", "fluid"], }) .div({ classes: ["columns", "is-centered", "is-vcentered", "is-mobile"], }) .div({ classes: ["column", "is-narrow"], styles: { width: "70%", }, });
As you can see, when setting up our container, we put this chainability
of Isotope to a good use. In the end, it's the last node in the chain that gets assigned to the container
variable. Also, notice how we use another configuration property - styles
- to set CSS styles of the underlying element.
At the moment our HTML structure should look somewhat like this:
<body> <main> <div> <div></div> </div> </main> </body>
Basic elements
Now that we've got the container, it's time to add some real elements to our app!
// ... container .h1({ classes: ["has-text-centered", "title"], }) .text("Isotope TODO"); container.form(); container.ul();
Here we're adding 3 new child nodes to the container: header, form, and list. Apart from the usual stuff, notice how we use a special text()
method to set the text of the created <h1>
element.
Now, after the header, we create two more elements - <form>
and <ul>
. These 2 elements are where the rest of our app will be placed. With this in mind, it's easy to see how our code can become bloated over time pretty easily. To prevent that, we'll move both of these elements into separate components
, which themselves will be placed within separate modules.
Creating components
In Isotope things are meant to be simple - and so are thecomponents, which themselves are nothing more than simple functions. Take a look:
// src/form.js const Form = (container) => { const form = container.form(); return form; }; export { Form };
Here, in a new file ( src/form.js
), we create a new Isotope component - Form
. As you can see, it's a function that accepts a parent node, and optionally returns a new node.
Such a component can then be used through the $()
method:
// src/index.js // ... import { Form } from "./form"; // ... container.$(Form);
If the component function returns a node, then the same node is returned from the $()
method. Otherwise, the $()
method returns the node it was called upon (in our case it would be the container
) for easier chaining.
As you can see, Isotope components are really easy to use. Let's now set up our List
component as well.
// src/list.js const List = (container) => { const list = container.ul(); return list; }; export { List };
// src/index.js // ... import { Form } from "./form"; import { List } from "./list"; // ... container.$(Form); container.$(List);
Building form
With our components set up, it's time to build our form for accepting new TODOs!
// src/index.js const Form = (container) => { const form = container.form({ classes: ["field", "has-addons"], styles: { justifyContent: "center" }, }); const input = form.div({ classes: ["control"] }).input({ attribs: { type: "text", placeholder: "TODO" }, classes: ["input"], }); form .div({ classes: ["control"] }) .button({ classes: ["button", "is-primary"] }) .span({ classes: ["icon", "is-small"] }) .i({ classes: ["fas", "fa-plus"] }); return form; }; export { Form };
So, above we create our form layout. As you can see, there's not much new when compared to what we already know. There's only the attribs
configuration property that's used to set attributes of the node's DOM element.
Apart from that, you can also notice how helpful Isotope's method chaining capabilities can be when creating the submit button.
Reactivity
With our form ready, we now need to make itreactive. Isotope is a statically-dynamic UI library, which (apart from sounding cool) means that it has a bit different approach to reactivity. Instead of making the entire UI reactive out-of-the-box, Isotope requires you to specifically mark certain nodes as dynamic by either creating their own state or by linking them to other dynamic nodes. For the purpose of our TODO app, we'll explore both of these ways.
First, we have to identify what kind of data should be made reactive. In our case - it's the list of TODOs that we'll operate on, and the current user input for creating new TODOs.
So, we've got 2 properties to create in our state
- input
and todos
. The state should be accessible by both the Form
(to write to input
), as well as List
(to display TODOs) component. Thus, I think it'll be best to initialize our state on the container
node.
// src/index.js // ... const container = view .main({ classes: ["container", "fluid"], }) .div({ classes: ["columns", "is-centered", "is-vcentered", "is-mobile"], }) .div({ classes: ["column", "is-narrow"], state: { input: "", todos: [], }, styles: { width: "70%", }, }); // ...
So, we go back to our index.js
file and set up our state on the last node (the one that's assigned to the container
variable. To do this, we make use of the state
property, supplying it with our state object, containing initial values. And that's it! - Now our container is reactive!
Event handling
Let's get back to the src/form.js
file and put this reactivity to good use. First, we'll handle the <form>
element itself.
// src/form.js const Form = (container) => { // ... form.on("submit", (event) => { const input = container.getState("input"); const todos = container.getState("todos"); if (input) { container.setState({ input: "", todos: [ ...todos, { text: input, id: Math.random().toString(36).substr(2, 9), }, ], }); } event.preventDefault(); }); // ... }; // ...
On the form
node, we use the on()
method to listen to the submit
event of the <form>
element. Isotope provides a set of event-related methods
( on()
, off()
and emit()
), which are universal and can be used to handle all kinds of events - DOM, custom, and Isotope-related ones.
In our handling function, we first access the input
and todos
properties from the container's state. Remember that Isotope doesn't handle data passing on its own - you need to do that by having a reference to a stateful node, through custom events or in any other way you find suitable. In our case, because the container
that holds the data is also the direct parent of our component, we can use that reference to access its state.
Isotope provides 2 methods to work with the state - getState()
and setState()
. To access one of state properties, you have to pass its key to the getState()
method. That's what we do to access the input
and todos
properties.
After that, we check whether the user has entered anything in the form (i.e. if the input
isn't empty) and if so, we transform it into a new TODO. In our case, a TODO is an object with text
and id
property, where text
contains TODO's actual content, and id
is a random string, to help us identify a given TODO later on.
We use the setState()
method to update the container
's state. The method accepts an object that should be applied on top of the previous state. It doesn't have to include all the properties the original state object had, but we assign both anyway. input
gets assigned an empty string to clean the value of <input>
element, while todos
is assigned a new array. Know that because arrays are passed by reference in JavaScript, you can as well use the push()
method on the todos
variable that we've got from the getState()
call. It's just a matter of personal preference as to which way you prefer. Just know that you'll eventually have to call the setState()
method (even with an empty object), to let Isotope know that it should update the node.
Lifecycle events
Now we'll move to our input
node to get it set up as well.
// src/form.js const Form = (container) => { // ... const input = form .div({ classes: ["control"] }) .input({ attribs: { type: "text", placeholder: "TODO" }, classes: ["input"], }) .on("input", ({ target }) => { container.setState({ input: target.value }); }) .on("node-updated", ({ node }) => { node.element.value = container.getState("input"); }); // ... }; // ...
Here, we once again use Isotope's chainability ( on()
method returns the node it was called upon) to listen to 2 events one after another. First, we handle the input
event, which is native to HTML <input>
element. Inside the listener, we use the setState()
method, to set the value of input
property to the current input.
Next up, we listen to one of Isotope's nodelifecycle events - node-updated
. This event is emitted every time a node updates - either via a change in state or in the result of a link. The listener is passed an object with node
property, giving it access to the node the listener is connected to. We use that reference to access the node's underlying HTML element through the element
property and set it's value to the value of input
property from the container's state.
Through the code above, we've gained complete control over the <input>
element. It's value is completely reliant on the value of the container
's state.
Linking
With the event listeners in place, our form is almost done. The last issue we have to solve is related to the node-updated
event our input
node is listening to. The problem is that it'll never be triggered as the node neither has its own state, nor it's linked to any other nodes.
To fix that issue, we have to write one magic line:
// src/form.js // ... container.link(input); // ...
With the use of the link()
method, we link
the input
node to the container
. Linking in Isotope allows us to let one node know that it should update when the other one does so. What we do with the line above is letting input
know that it should update (thus triggering the node-updated
event) every time the container
's state is changed.
It's important to remember that linking can happen between any 2 nodes - no matter where they are in the hierarchy. A single node can have multiple nodes linked to itself, but it can be linked only to a single node.
Displaying TODOs
Now that our form is ready and can accept new TODOs, we have to take care of displaying them.
Let's get back to our List
component and start our work:
// src/list.js const List = (container) => { const list = container.ul({ classes: () => ({ list: container.getState("todos").length > 0, }), }); container.link(list); return list; }; export { List };
First, we make a few changes to our base list
node. We use the classes
configuration property, but in a bit different way than usual. Instead of passing an array of CSS class names, we pass a function, which returns an object. In this way, we let Isotope know that it should rerun the function and update CSS classes every time the node updates
. The value that the function returns gets later applied like usual.
An object that the function returns is an alternative way of applying CSS class names. The object's keys represent certain CSS class names and their values - booleans that indicate whether the given CSS class should be applied or removed. As a side note, other configuration properties ( attribs
and styles
) also accept a similar function configuration.
So, we apply the "list"
CSS class name only when our TODOs list contains at least one TODO. But, in order for our dynamic classes
to work, we also have to link the list
node to the container
, which we do in the next line.
List rendering
Now that we've got our <ul>
element set up, we only need to display our TODOs. In Isotope, this can be done with a special map()
method.
// src/list.js // ... list.map( () => container.getState("todos"), ({ id, text }, node) => { const item = node.li({ classes: ["list-item"] }); const itemContainer = item.div({ classes: ["is-flex"], styles: { alignItems: "center" }, }); itemContainer.span({ classes: ["is-pulled-left"] }).text(text); itemContainer.div({ styles: { flex: "1" } }); itemContainer .button({ classes: ["button", "is-text", "is-pulled-right", "is-small"], }) .on("click", () => { const todos = container.getState("todos"); const index = todos.findIndex((todo) => todo.id === id); container.setState("todos", todos.splice(index, 1)); }) .span({ classes: ["icon"] }) .i({ classes: ["fas", "fa-check"] }); return item; } ); // ...
map()
takes 2 arguments - the list of items to map and a function used to map them. The items list can have multiple forms. For static lists it can be an array of unique strings, numbers or objects with an id
key. For dynamic lists, where items get modified on the way, you can pass parent's state property key, or a function that determines the items, as we do above. Because todos
is a property of container
's state - not the list
's, a function is the only solution we have.
Inside the mapping function, we get access to the current item (in our case items are objects with text
and id
properties), the parent node ( list
) and the index of the current item. We only use 2 of those values.
Overall, the rest of the code is nothing new - we create nodes, set their CSS classes, styles, attributes and text, and listen to the click
event on the button, to remove a certain TODO when needed.
What do you think?
So, with that, our TODO app is ready. You can check out the finished results through the CodeSandbox playground, right here:
To summarize, through making this very simple app, we've learned pretty much most of the Isotope API. That's right - it's that simple. Remember that although the API and the library itself is small and simple, it can still be used to create really incredible and very performant apps and websites!
If you like what you see, definitely check out Isotope's documentation , and drop a star on its GitHub repo !
For more content about Isotope and web development as a whole, follow me on Twitter , Facebook or through my newsletter .
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。