You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

546 lines
16 KiB

4 years ago
  1. # dom-bindings
  2. [![Build Status][travis-image]][travis-url]
  3. [![Code Quality][codeclimate-image]][codeclimate-url]
  4. [![NPM version][npm-version-image]][npm-url]
  5. [![NPM downloads][npm-downloads-image]][npm-url]
  6. [![MIT License][license-image]][license-url]
  7. [![Coverage Status][coverage-image]][coverage-url]
  8. ## Usage
  9. ```js
  10. import { template, expressionTypes } from '@riotjs/dom-bindings'
  11. // Create the app template
  12. const tmpl = template('<p><!----></p>', [{
  13. selector: 'p',
  14. expressions: [
  15. {
  16. type: expressionTypes.TEXT,
  17. childNodeIndex: 0,
  18. evaluate: scope => scope.greeting,
  19. },
  20. ],
  21. }])
  22. // Mount the template to any DOM node
  23. const target = document.getElementById('app')
  24. const app = tmpl.mount(target, {
  25. greeting: 'Hello World'
  26. })
  27. ```
  28. [travis-image]:https://img.shields.io/travis/riot/dom-bindings.svg?style=flat-square
  29. [travis-url]:https://travis-ci.org/riot/dom-bindings
  30. [license-image]:http://img.shields.io/badge/license-MIT-000000.svg?style=flat-square
  31. [license-url]:LICENSE
  32. [npm-version-image]:http://img.shields.io/npm/v/@riotjs/dom-bindings.svg?style=flat-square
  33. [npm-downloads-image]:http://img.shields.io/npm/dm/@riotjs/dom-bindings.svg?style=flat-square
  34. [npm-url]:https://npmjs.org/package/@riotjs/dom-bindings
  35. [coverage-image]:https://img.shields.io/coveralls/riot/dom-bindings/master.svg?style=flat-square
  36. [coverage-url]:https://coveralls.io/r/riot/dom-bindings/?branch=master
  37. [codeclimate-image]:https://api.codeclimate.com/v1/badges/d0b7c555a1673354d66f/maintainability
  38. [codeclimate-url]:https://codeclimate.com/github/riot/dom-bindings/maintainability
  39. ## API
  40. ### template(String, Array)
  41. The template method is the most important of this package.
  42. It will create a `TemplateChunk` that could be mounted, updated and unmounted to any DOM node.
  43. <details>
  44. <summary>Details</summary>
  45. A template will always need a string as first argument and a list of `Bindings` to work properly.
  46. Consider the following example:
  47. ```js
  48. const tmpl = template('<p><!----></p>', [{
  49. selector: 'p',
  50. expressions: [
  51. {
  52. type: expressionTypes.TEXT,
  53. childNodeIndex: 0,
  54. evaluate: scope => scope.greeting
  55. }
  56. ],
  57. }])
  58. ```
  59. The template object above will bind a [simple binding](#simple-binding) to the `<p>` tag.
  60. </details>
  61. ### bindingTypes
  62. Object containing all the type of bindings supported
  63. ### expressionTypes
  64. Object containing all the expressions types supported
  65. ## Bindings
  66. A binding is simply an object that will be used internally to map the data structure provided to a DOM tree.
  67. <details>
  68. <summary>Details</summary>
  69. To create a binding object you might use the following properties:
  70. - `expressions`
  71. - type: `Array<Expression>`
  72. - required: `true`
  73. - description: array containing instructions to execute DOM manipulation on the node queried
  74. - `type`
  75. - type: `Number`
  76. - default:`bindingTypes.SIMPLE`
  77. - optional: `true`
  78. - description: id of the binding to use on the node queried. This id must be one of the keys available in the `bindingTypes` object
  79. - `selector`
  80. - type: `String`
  81. - default: binding root **HTMLElement**
  82. - optional: `true`
  83. - description: property to query the node element that needs to updated
  84. The bindings supported are only of 4 different types:
  85. - [`simple`](#simple-binding) to bind simply the expressions to a DOM structure
  86. - [`each`](#each-binding) to render DOM lists
  87. - [`if`](#if-binding) to handle conditional DOM structures
  88. - [`tag`](#tag-binding) to mount a coustom tag template to any DOM node
  89. Combining the bindings above we can map any javascript object to a DOM template.
  90. </details>
  91. ### Simple Binding
  92. These kind of bindings will be only used to connect the expressions to DOM nodes in order to manipulate them.
  93. <details>
  94. <summary>Details</summary>
  95. **Simple bindings will never modify the DOM tree structure, they will only target a single node.**<br/>
  96. A simple binding must always contain at least one of the following expression:
  97. - `attribute` to update the node attributes
  98. - `event` to set the event handling
  99. - `text` to update the node content
  100. - `value` to update the node value
  101. For example, let's consider the following binding:
  102. ```js
  103. const pGreetingBinding = {
  104. selector: 'p',
  105. expressions: [{
  106. type: expressionTypes.Text,
  107. childNodeIndex: 0,
  108. evaluate: scope => scope.greeting,
  109. }]
  110. }
  111. template('<article><p><!----></p></article>', [pGreeting])
  112. ```
  113. In this case we have created a binding to update only the content of a `p` tag.<br/>
  114. *Notice that the `p` tag has an empty comment that will be replaced with the value of the binding expression whenever the template will be mounted*
  115. </details>
  116. #### Simple Binding Expressions
  117. The simple binding supports DOM manipulations only via expressions.
  118. <details>
  119. <summary>Details</summary>
  120. An expression object must have always at least the following properties:
  121. - `evaluate`
  122. - type: `Function`
  123. - description: function that will receive the current template scope and will return the current expression value
  124. - `type`
  125. - type: `Number`
  126. - description: id to find the expression we need to apply to the node. This id must be one of the keys available in the `expressionTypes` object
  127. </details>
  128. ##### Attribute Expression
  129. The attribute expression allows to update all the DOM node attributes.
  130. <details>
  131. <summary>Details</summary>
  132. This expression might contain the optional `name` key to update a single attribute for example:
  133. ```js
  134. // update only the class attribute
  135. { type: expressionTypes.ATTRIBUTE, name: 'class', evaluate(scope) { return scope.attr }}
  136. ```
  137. If the `name` key will not be defined and the return of the `evaluate` function will be an object, this expression will set all the pairs `key, value` as DOM attributes. <br/>
  138. Given the current scope `{ attr: { class: 'hello', 'name': 'world' }}`, the following expression will allow to set all the object attributes:
  139. ```js
  140. { type: expressionTypes.ATTRIBUTE, evaluate(scope) { return scope.attr }}
  141. ```
  142. If the return value of the evaluate function will be a `Boolean` the attribute will be considered a boolean attribute like `checked` or `selected`...
  143. </details>
  144. ##### Event Expression
  145. The event expression is really simple, It must contain the `name` attribute and it will set the callback as `dom[name] = callback`.
  146. <details>
  147. <summary>Details</summary>
  148. For example:
  149. ```js
  150. // add an event listener
  151. { type: expressionTypes.EVENT, name: 'onclick', evaluate(scope) { return function() { console.log('Hello There') } }}
  152. ```
  153. To remove an event listener you should only `return null` via evaluate function:
  154. ```js
  155. // remove an event listener
  156. { type: expressionTypes.EVENT, name: 'onclick', evaluate(scope) { return null } }}
  157. ```
  158. </details>
  159. ##### Text Expression
  160. The text expression must contain the `childNodeIndex` that will be used to identify which childNode from the `element.childNodes` collection will need to update its text content.
  161. <details>
  162. <summary>Details</summary>
  163. Given for example the following template:
  164. ```html
  165. <p><b>Your name is:</b><i>user_icon</i><!----></p>
  166. ```
  167. we could use the following text expression to replace the CommentNode with a TextNode
  168. ```js
  169. { type: expressionTypes.TEXT, childNodeIndex: 2, evaluate(scope) { return 'Gianluca' } }}
  170. ```
  171. </details>
  172. ##### Value Expression
  173. The value expression will just set the `element.value` with the value received from the evaluate function.
  174. <details>
  175. <summary>Details</summary>
  176. It should be used only for form elements and it might look like the example below:
  177. ```js
  178. { type: expressionTypes.VALUE, evaluate(scope) { return scope.val }}
  179. ```
  180. </details>
  181. ### Each Binding
  182. The `each` binding is used to create multiple DOM nodes of the same type. This binding is typically used in to render javascript collections.
  183. <details>
  184. <summary>Details</summary>
  185. **`each` bindings will need a template that will be cloned, mounted and updated for all the instances of the collection.**<br/>
  186. An each binding should contain the following properties:
  187. - `itemName`
  188. - type: `String`
  189. - required: `true`
  190. - description: name to identify the item object of the current iteration
  191. - `indexName`
  192. - type: `Number`
  193. - optional: `true`
  194. - description: name to identify the current item index
  195. - `evaluate`
  196. - type: `Function`
  197. - required: `true`
  198. - description: function that will return the collection to iterate
  199. - `template`
  200. - type: `TemplateChunk`
  201. - required: `true`
  202. - description: a dom-bindings template that will be used as skeleton for the DOM elements created
  203. - `condition`
  204. - type: `Function`
  205. - optional: `true`
  206. - description: function that can be used to filter the items from the collection
  207. The each bindings have the highest [hierarchical priority](#bindings-hierarchy) compared to the other riot bindings.
  208. The following binding will loop through the `scope.items` collection creating several `p` tags having as TextNode child value dependent loop item received
  209. ```js
  210. const eachBinding = {
  211. type: bindingTypes.EACH,
  212. itemName: 'val',
  213. indexName: 'index'
  214. evaluate: scope => scope.items,
  215. template: template('<!---->', [{
  216. expressions: [
  217. {
  218. type: expressionTypes.TEXT,
  219. childNodeIndex: 0,
  220. evaluate: scope => `${scope.val} - ${scope.index}`
  221. }
  222. ]
  223. }
  224. }
  225. template('<p></p>', [eachBinding])
  226. ```
  227. </details>
  228. ### If Binding
  229. The `if` bindings are needed to handle conditionally entire parts of your components templates
  230. <details>
  231. <summary>Details</summary>
  232. **`if` bindings will need a template that will be mounted and unmounted depending on the return value of the evaluate function.**<br/>
  233. An if binding should contain the following properties:
  234. - `evaluate`
  235. - type: `Function`
  236. - required: `true`
  237. - description: if this function will return truthy values the template will be mounted otherwise unmounted
  238. - `template`
  239. - type: `TemplateChunk`
  240. - required: `true`
  241. - description: a dom-bindings template that will be used as skeleton for the DOM element created
  242. The following binding will render the `b` tag only if the `scope.isVisible` property will be truthy. Otherwise the `b` tag will be removed from the template
  243. ```js
  244. const ifBinding = {
  245. type: bindingTypes.IF,
  246. evaluate: scope => scope.isVisible,
  247. selector: 'b'
  248. template: template('<!---->', [{
  249. expressions: [
  250. {
  251. type: expressionTypes.TEXT,
  252. childNodeIndex: 0,
  253. evaluate: scope => scope.name
  254. }
  255. ]
  256. }])
  257. }
  258. template('<p>Hello there <b></b></p>', [ifBinding])
  259. ```
  260. </details>
  261. ### Tag Binding
  262. The `tag` bindings are needed to mount custom components implementations
  263. <details>
  264. <summary>Details</summary>
  265. `tag` bindings will enhance any child node with a custom component factory function. These bindings are likely riot components that must be mounted as children in a parent component template
  266. A tag binding might contain the following properties:
  267. - `getComponent`
  268. - type: `Function`
  269. - required: `true`
  270. - description: the factory function responsible for the tag creation
  271. - `evaluate`
  272. - type: `Function`
  273. - required: `true`
  274. - description: it will receive the current scope and it must return the component id that will be passed as first argument to the `getComponent` function
  275. - `slots`
  276. - type: `Array<Slot>`
  277. - optional: `true`
  278. - description: array containing the slots that must be mounted into the child tag
  279. - `attributes`
  280. - type: `Array<AttributeExpression>`
  281. - optional: `true`
  282. - description: array containing the attribute values that should be passed to the child tag
  283. The following tag binding will upgrade the `time` tag using the `human-readable-time` template.
  284. This is how the `human-readable-time` template might look like
  285. ```js
  286. import moment from 'moment'
  287. export default function HumanReadableTime({ attributes }) {
  288. const dateTimeAttr = attributes.find(({ name }) => name === 'datetime')
  289. return template('<!---->', [{
  290. expressions: [{
  291. type: expressionTypes.TEXT,
  292. childNodeIndex: 0,
  293. evaluate(scope) {
  294. const dateTimeValue = dateTimeAttr.evaluate(scope)
  295. return moment(new Date(dateTimeValue)).fromNow()
  296. }
  297. }, ...attributes.map(attr => {
  298. return {
  299. ...attr,
  300. type: expressionTypes.ATTRIBUTE
  301. }
  302. })]
  303. }])
  304. }
  305. ```
  306. Here it's how the previous tag might be used in a `tag` binding
  307. ```js
  308. import HumanReadableTime from './human-readable-time'
  309. const tagBinding = {
  310. type: bindingTypes.TAG,
  311. evaluate: () => 'human-readable-time',
  312. getComponent: () => HumanReadableTime,
  313. selector: 'time',
  314. attributes: [{
  315. evaluate: scope => scope.time,
  316. name: 'datetime'
  317. }]
  318. }
  319. template('<p>Your last commit was: <time></time></p>', [tagBinding]).mount(app, {
  320. time: '2017-02-14'
  321. })
  322. ```
  323. The `tag` bindings have always a lower priority compared to the `if` and `each` bindings
  324. </details>
  325. #### Slot Binding
  326. The slot binding will be used to manage nested slotted templates that will be update using parent scope
  327. <details>
  328. <summary>Details</summary>
  329. An expression object must have always at least the following properties:
  330. - `evaluate`
  331. - type: `Function`
  332. - description: function that will receive the current template scope and will return the current expression value
  333. - `type`
  334. - type: `Number`
  335. - description: id to find the expression we need to apply to the node. This id must be one of the keys available in the `expressionTypes` object
  336. - `name`
  337. - type: `String`
  338. - description: the name to identify the binding html we need to mount in this node
  339. ```js
  340. // slots array that will be mounted receiving the scope of the parent template
  341. const slots = [{
  342. id: 'foo',
  343. bindings: [{
  344. selector: '[expr1]',
  345. expressions: [{
  346. type: expressionTypes.TEXT,
  347. childNodeIndex: 0,
  348. evaluate: scope => scope.text
  349. }]
  350. }],
  351. html: '<p expr1><!----></p>'
  352. }]
  353. const el = template('<article><slot expr0/></article>', [{
  354. type: bindingTypes.SLOT,
  355. selector: '[expr0]',
  356. name: 'foo'
  357. }]).mount(app, {
  358. slots
  359. }, { text: 'hello' })
  360. ```
  361. </details>
  362. ## Bindings Hierarchy
  363. If the same DOM node has multiple bindings bound to it, they should be created following the order below:
  364. 1. Each Binding
  365. 2. If Binding
  366. 3. Tag Binding
  367. <details>
  368. <summary>Details</summary>
  369. Let's see some cases where we might combine multiple bindings on the same DOM node and how to handle them properly.
  370. ### Each and If Bindings
  371. Let's consider for example a DOM node that sould handle in parallel the Each and If bindings.
  372. In that case we could skip the `If Binding` and just use the `condition` function provided by the [`Each Binding`](#each-binding)
  373. Each bindings will handle conditional rendering internally without the need of extra logic.
  374. ### Each and Tag Bindings
  375. A custom tag having an Each Binding bound to it should be handled giving the priority to the Eeach Binding. For example:
  376. ```js
  377. const components = {
  378. 'my-tag': function({ slots, attributes }) {
  379. return {
  380. mount(el, scope) {
  381. // do stuff on the mount
  382. },
  383. unmount() {
  384. // do stuff on the unmount
  385. }
  386. }
  387. }
  388. }
  389. const el = template('<ul><li expr0></li></ul>', [{
  390. type: bindingTypes.EACH,
  391. itemName: 'val',
  392. selector: '[expr0]',
  393. evaluate: scope => scope.items,
  394. template: template(null, [{
  395. type: bindingTypes.TAG,
  396. name: 'my-tag',
  397. getComponent(name) {
  398. // name here will be 'my-tag'
  399. return components[name]
  400. }
  401. }])
  402. }]).mount(target, { items: [1, 2] })
  403. ```
  404. The template for the Each Binding above will be created receiving `null` as first argument because we suppose that the custom tag template was already stored and registered somewhere else.
  405. ### If and Tag Bindings
  406. Similar to the previous example, If Bindings have always the priority on the Tag Bindings. For example:
  407. ```js
  408. const el = template('<ul><li expr0></li></ul>', [{
  409. type: bindingTypes.IF,
  410. selector: '[expr0]',
  411. evaluate: scope => scope.isVisible,
  412. template: template(null, [{
  413. type: bindingTypes.TAG,
  414. evaluate: () => 'my-tag',
  415. getComponent(name) {
  416. // name here will be 'my-tag'
  417. return components[name]
  418. }
  419. }])
  420. }]).mount(target, { isVisible: true })
  421. ```
  422. The template for the IF Binding will mount/unmount the Tag Binding on its own DOM node.
  423. </details>