0.6.5 • Published 5 years ago

@thoughtsunificator/domodel v0.6.5

Weekly downloads
-
License
MIT
Repository
github
Last release
5 years ago

domodel Build Status Gitpod Ready-to-Code

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

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 to createElement
  • children - Array - To add children to an Element
  • identifier - String - To save and retrieve a Node
  • model - Model - Specify the model that should be ran
  • binding - 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 binding
  • root The root Element of your model
  • identifier 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 target

  • INSERT_BEFORE Insert your model before the target

  • REPLACE_ELEMENT Replace the target with your model

  • WRAP_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 observable
  • listen 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.

0.6.5

5 years ago

0.6.4

5 years ago

0.6.3

5 years ago

0.6.2

5 years ago