@thoughtsunificator/domodel v0.6.5
domodel

domodel is library that helps build user interfaces that are easier to think about and maintain. Following the separation of concerns design principle, domodel makes a clear distinction in your code between the look (model) and behavior (binding) of DOM Elements. domodel also offers a modern way to build your application with ES6 native modules.
That's it! All with the mindset of embracing both JavaScript and HTML semantics.
Summary
- Getting started - Prerequisites - Installing - Scaffold - Standalone - Node.js - Model - Super Model - Custom properties - Binding - Exposed properties - Adding models to the DOM and managing them - Methods - Observables - Main observable - Listening to events - Emitting events - Removing event listeners
- Advanced - Nesting models - Referencing to nested models
- API
- Extensions
- Demos
- Running the tests
Getting started
Prerequisites
None!
Installing
Scaffold
To get started with domodel all you need to do is scaffold a new project by following the instructions over at the domodel-skeleton repository.
Standalone
To be able to use domodel in the browser you can use git submodules.
Run git submodule add https://github.com/thoughtsunificator/domodel lib/domodel
at the root of your project, the library will then be located at /lib
.
Node.js
npm install @thoughtsunificator/domodel
import { DOModel } from "@thoughtsunificator/domodel"
Model
A model is nothing more than the JSON representation of an element.
Let's take this simple example:
export default {
tagName: "button"
}
Would the equivalent of:
const button = document.createElement("button")
Wants to add children to a node ? No problem:
src/model/model.js
export default {
tagName: "div",
children: [
{
tagName: "h2",
identifier: "headline",
textContent: "Unveil a new world"
}
]
}
Notice the textContent
property, yes, you can use whatever Element property of the DOM API..
The identifier property will enable you to later retrieve your h2 Element.
- The term model will be later used to refer to both the model and its binding to make it simpler.
Super model
You usually wants to incorporate all your models inside a super model
so that you can pass it a global observable that will serve for all your models to communicate with each others.
The super model is usually ran in src/app.js
Custom properties
Most properties listed in your model are defined at the Element level.
However custom properties are not set on the Element as they have unusual behaviors they are treated differently:
tagName
- String - Passed tocreateElement
children
- Array - To add children to an Elementidentifier
- String - To save and retrieve a Nodemodel
- Model - Specify the model that should be ranbinding
- Binding - Specify the binding to use when running the model (model
property must be set)props
- Object - Specify the arguments to pass along the binding (binding
property must be set)
Binding
Now that we're to define models, would not it be cool to be able to do all sorts of thing with those models ? Like adding them to the DOM.
Exposed properties
These properties are always available from within an instance of a Binding be it inside onCreated, onCompleted or anything else:
props
The arguments you passed when creating the instance of your bindingroot
The root Element of your modelidentifier
Enable you to access individual Element inside your models.
Adding models to the DOM and managing them
We might know how to define models however they wont simply be added by defining them alone.
You have to use the run method provided by DOModel object and tell it how to add them.
The first step in your project would be create or edit the app.js
in src/
, it is the entry point module that is defined in your index.html
.
src/app.js
import { DOModel } from "../lib/domodel/index.js" // first we're importing DOModel
import YourModelModel from "./model/yourmodel.js" // the model we defined earlier, it is our super model
import YourModelBinding from "./binding/yourmodel.js" // the binding we will be defining later
window.addEventListener("load", function() { // we only add the
DOModel.run(YourModelModel, {
method: DOModel.METHOD.APPEND_CHILD, // This is the default method and will append the children to the parentNode.
binding: new YourModelBinding({ myProp: "hello :)" }), // we're creating an instance of our binding (which extends the Binding class provided by DOModel) and passing it to the run method.
parentNode: document.body // the node we want to update in this case it is the node where we want to append the child node using appendChild.
})
})
Now that your app.js
is created let's create your first binding:
src/binding/yourmodel.js
import { DOModel } from "../../lib/domodel/index.js" // you could import the library again and run yet another model inside this model
export default class extends Binding {
async onCreated() {
const { myProp } = this.props
console.log(myProp) // prints hello
// access your model root element through the root property: this.root
// access identifier with the identifier property:
this.identifier.headline.textContent = "The new world was effectively unveiled before my very eyes"
// you might even run another model inside this model
}
}
Methods
The following methods change how a model will be run.
APPEND_CHILD
Append your model to the targetINSERT_BEFORE
Insert your model before the targetREPLACE_ELEMENT
Replace the target with your modelWRAP_ELEMENT
Wrap the target inside your model
The target being the parentNode
argument specified when using run
method.
They are available through DOModel.METHOD
.
Observables
Observable are the classes your define to hold the non-visual logics about your models, they all extend the abstract Observable
class provided by DOModel.
Usually you will be defining your observable in the object/
folder:
src/object/observable-example.js
import { Observable } from "../lib/domodel/index.js"
export default class extends Observable {
// you can have a constructor
// getter setter...
// or even better, you could have methods.
}
Main observable
You usually wants to have a main observable
that you will pass to most of your models so that they communicate with each other through a central.
Note that you are not forced to have one and you could have multiple observables and still be able to handle inter-model communication.
Most of the time we call it application
.
Listening to events
Your Observable inherits two methods:
emit
To emit events to an observablelisten
To listen to events emitted to your observable
src/binding/model.js
import Game from "../object/game.js"
export default class {
async onCreated() {
const game = new Game()
game.listen("message", async data => {
console.log(data)
})
}
}
Emitting events
src/binding/model.js
import Game from "../object/game.js"
export default class {
async onCreated() {
const game = new Game()
game.emit("message", { /* data go here */ })
}
}
Removing event listeners
Sometimes you might want your model to be dynamically added and removed, meaning that it will be added upon an action and removed upon another action.
Usually what you want to do is to create _listener
variable and push all the listeners to this array and then remove them as needed using forEach
for example.
In this example, we create a listener message
and remove it whenever the event done
is emitted.
src/binding/model.js
import Game from "../object/game.js"
export default class {
async onCreated() {
const game = new Game()
const _listeners = []
_listeners.push(game.listen("message", async data => {
console.log(data)
}))
_listeners(game.listen("done", async data => {
_listeners.forEach(listener => listener.remove())
}))
}
}
Remember that listeners are bound to an object.
Advanced
Nesting models
You could you use import:
src/model/main-model.js
import MyModel from "./my-model.js"
export default {
tagName: "div",
children: [
MyModel
]
}
Using callbacks where data
is your model:
src/model/main-model.js
export default data => ({
tagName: "div",
children: [
data
]
})
You could also use bindings to do just that:
src/binding/main-model.js
import { DOModel } from "../../lib/domodel/index.js"
import MyModel from "../model/my-model.js"
import MyBinding from "./my-model.js"
export default class extends Binding {
onCreated() {
DOModel.run(MyModel, { parentNode: this.root, binding: new MyBinding() })
}
}
Or you could use the model
custom property provided by domodel:
src/model/main-model.js
import MyModel from "./my-model.js"
import MyBinding from "../binding/my-model.js"
export default {
tagName: "div",
children: [
{
model: MyModel,
binding: MyBinding // optionnal
props: {} // optionnal
identifier: "model" // optionnal
// Any other property is not handled.
}
]
}
What happens is that DOModel will be itself calling the run
method with the argument you provided.
The hierarchy of nodes stops here and continue in the model you specified.
Referencing to nested models
In some cases, you might want to reference to a nested model.
You can use the identifier
, it will reference to an instance of the binding you specified, in this case it would be an instance of MyBinding
.
Accessing the reference:
src/binding/my-model.js
import { Binding } from "../../lib/domodel/index.js" // you could import the library again and run yet another model inside this model
export default class extends Binding {
async onCreated() {
console.log(this.identifier.model) // returns an instance of MyBinding
// You could access the root element of the nested model through:
console.log(this.identifier.model.root)
// and much more...
}
}
API
You can read the API by visiting https://thoughtsunificator.github.io/domodel.
Extensions
Demos
Running the tests
npm install
Then simply run npm test
.