├── .gitignore ├── .storybook ├── main.js └── preview-head.html ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── context-element.html ├── docs ├── favicon.ico ├── iframe.html ├── index.html ├── main.3991cff3ec22f54eb0e1.bundle.js ├── main.d66ddc34bb951c6d083d.bundle.js ├── main.d66ddc34bb951c6d083d.bundle.js.map ├── runtime~main.286b69a873d49f2f199c.bundle.js ├── runtime~main.d66ddc34bb951c6d083d.bundle.js ├── runtime~main.d66ddc34bb951c6d083d.bundle.js.map ├── sb_dll │ ├── storybook_ui-manifest.json │ ├── storybook_ui_dll.LICENCE │ └── storybook_ui_dll.js ├── vendors~main.85cc07c9616c9688c607.bundle.js ├── vendors~main.d66ddc34bb951c6d083d.bundle.js ├── vendors~main.d66ddc34bb951c6d083d.bundle.js.LICENSE.txt └── vendors~main.d66ddc34bb951c6d083d.bundle.js.map ├── example ├── action │ └── input-change.html ├── array │ └── cities.html ├── color-palette │ ├── index.css │ ├── index.html │ └── index.js ├── nested-element │ └── nested-element.html ├── todo │ ├── index.ts │ └── todo.html └── watch │ └── time.html ├── index.js ├── index.min.js ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── array-context-element.spec.ts ├── array-context-element.ts ├── context-element.spec.ts ├── context-element.ts ├── index.ts ├── libs │ ├── attribute-evaluator.ts │ ├── attribute-validator.spec.ts │ ├── attribute-validator.ts │ ├── data-renderer.ts │ ├── error-message.ts │ ├── no-empty-text-node.ts │ └── uuid.ts ├── stories │ ├── context-array │ │ ├── input.stories.ts │ │ └── timer.stories.ts │ ├── context-element │ │ ├── checkbox.stories.ts │ │ ├── input.stories.ts │ │ └── timer.stories.ts │ ├── todo.stories.ts │ └── useJavascript.ts └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /.cache 4 | /.idea 5 | /coverage 6 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | stories: ['../src/**/*.stories.[tj]s'], 4 | addons: ['@storybook/preset-typescript', 5 | '@storybook/addon-knobs/register', 6 | { 7 | name: '@storybook/addon-storysource', 8 | options: { 9 | rule: { 10 | include: [path.resolve(__dirname, '../src')], // You can specify directories 11 | }, 12 | loaderOptions: { 13 | prettierConfig: { printWidth: 80, singleQuote: false }, 14 | }, 15 | }, 16 | }, 17 | ] 18 | }; -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | notifications: 4 | email: a.arif.r@gmail.com 5 | 6 | jobs: 7 | include: 8 | # Define the release stage that runs semantic-release 9 | - stage: release 10 | node_js: lts/* 11 | # Advanced: optionally overwrite your default `script` step to skip the tests 12 | # script: skip 13 | deploy: 14 | provider: script 15 | skip_cleanup: true 16 | script: 17 | - npx semantic-release -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 1. Make sure the test case coverage should not drop from 95%. If you add features or fixes, make sure the code is covered in your test scenario. 10 | 2. Update the README.md if needed. 11 | 3. Always use "npm run commit" (comittizen) to commit your changes. Versioning of the build release uses [semantic-release](https://github.com/semantic-release/semantic-release). 12 | 13 | ## Code of Conduct 14 | 15 | ### Our Pledge 16 | 17 | In the interest of fostering an open and welcoming environment, we as 18 | contributors and maintainers pledge to making participation in our project and 19 | our community a harassment-free experience for everyone, regardless of age, body 20 | size, disability, ethnicity, gender identity and expression, level of experience, 21 | nationality, personal appearance, race, religion, or sexual identity and 22 | orientation. 23 | 24 | ### Our Standards 25 | 26 | Examples of behavior that contributes to creating a positive environment 27 | include: 28 | 29 | * Using welcoming and inclusive language 30 | * Being respectful of differing viewpoints and experiences 31 | * Gracefully accepting constructive criticism 32 | * Focusing on what is best for the community 33 | * Showing empathy towards other community members 34 | 35 | Examples of unacceptable behavior by participants include: 36 | 37 | * The use of sexualized language or imagery and unwelcome sexual attention or 38 | advances 39 | * Trolling, insulting/derogatory comments, and personal or political attacks 40 | * Public or private harassment 41 | * Publishing others' private information, such as a physical or electronic 42 | address, without explicit permission 43 | * Other conduct which could reasonably be considered inappropriate in a 44 | professional setting 45 | 46 | ### Our Responsibilities 47 | 48 | Project maintainers are responsible for clarifying the standards of acceptable 49 | behavior and are expected to take appropriate and fair corrective action in 50 | response to any instances of unacceptable behavior. 51 | 52 | Project maintainers have the right and responsibility to remove, edit, or 53 | reject comments, commits, code, wiki edits, issues, and other contributions 54 | that are not aligned to this Code of Conduct, or to ban temporarily or 55 | permanently any contributor for other behaviors that they deem inappropriate, 56 | threatening, offensive, or harmful. 57 | 58 | ### Scope 59 | 60 | This Code of Conduct applies both within project spaces and in public spaces 61 | when an individual is representing the project or its community. Examples of 62 | representing a project or community include using an official project e-mail 63 | address, posting via an official social media account, or acting as an appointed 64 | representative at an online or offline event. Representation of a project may be 65 | further defined and clarified by project maintainers. 66 | 67 | ### Enforcement 68 | 69 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 70 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 71 | complaints will be reviewed and investigated and will result in a response that 72 | is deemed necessary and appropriate to the circumstances. The project team is 73 | obligated to maintain confidentiality with regard to the reporter of an incident. 74 | Further details of specific enforcement policies may be posted separately. 75 | 76 | Project maintainers who do not follow or enforce the Code of Conduct in good 77 | faith may face temporary or permanent repercussions as determined by other 78 | members of the project's leadership. 79 | 80 | ### Attribution 81 | 82 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 83 | available at [http://contributor-covenant.org/version/1/4][version] 84 | 85 | [homepage]: http://contributor-covenant.org 86 | [version]: http://contributor-covenant.org/version/1/4/ 87 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # _context-element_ 2 | 3 | [![Build Status](https://travis-ci.org/marsa-emreef/context-element.svg?branch=master)](https://travis-ci.org/marsa-emreef/context-element) 4 | ![gzip size](http://img.badgesize.io/https://unpkg.com/context-element/index.min.js?compression=gzip&label=MinJS%20gzip%20size) 5 | [![codecov](https://codecov.io/gh/marsa-emreef/context-element/branch/master/graph/badge.svg)](https://codecov.io/gh/marsa-emreef/context-element) 6 | ![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/marsa-emreef/context-element/master) 7 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 8 | ![GitHub](https://img.shields.io/github/license/marsa-emreef/context-element) 9 | 10 | 11 | **_`context-element`_** is an HTMLElement that makes it easy to render data or array in html page. 12 | 13 | **_`context-element`_** is a very small ![gzip size](http://img.badgesize.io/https://unpkg.com/context-element/index.min.js?compression=gzip&label=MinJS%20gzip%20size). It can render an array of data efficiently and quickly. You can directly use **_`context-element`_** on the html page by supplying `arrays or object` to the attribute `data` into the **_`context-element`_** element. You can determine how the data will be displayed by creating a template inside the **_`context-element`_**. 14 | 15 | ### installation 16 | 17 | From CDN 18 | ```html 19 | 20 | ``` 21 | 22 | npm module 23 | ``` 24 | npm install context-element 25 | ``` 26 | 27 | ## Why context-element 28 | 1. Because context-elements are native HTMLElement. 29 | 2. Because famous UI Framework cuts your bandwidth budget. 30 | 3. Because it doesn't use virtual dom, memory usage is very efficient. 31 | 4. Because the syntax is very simple, and the same source-code can be used by UX designer tools. 32 | 5. Because it has been proven by using reducer, it is easier to monitor app behaviour by using one-way data flow. 33 | 34 | ## Motivation 35 | Currently to be able to render objects or arrays on html pages we can use the template engine, or UI framework. 36 | 37 | Unfortunately template engine forces us to use syntax that is not the same as how elements in html are structured. 38 | 39 | In addition to the UI Framework library sometimes has a file size that is not small, we also have to download the framework tools to start the project. 40 | 41 | WebComponent aims to enable us to create new elements that are recognized by the browser. It would be great, if there is a web component, which uses the html structure as templating, and works efficiently like the UI Framework. 42 | 43 | **_`context-element`_** have similarities as frameworks, but context-elements are not frameworks engine, rather than simple **`HTMLElement`** that can organize how data is displayed based on templates. 44 | 45 | 46 | ```html 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
Current time is
55 |
56 |
57 | 65 | 66 | 67 | ``` 68 | 69 | 70 | ## How it works 71 | context-element has a property called `data`. The `data` property in context-element is also known as the `context-data`. 72 | When we supply values to the context-element `context-data`, the context-element will automatically re-render its template. 73 | 74 | To set `context-data` values, 75 | We can do it _imperatively_ via javascript 76 | ```javascript 77 | contextElement.data = { time:new Date() } 78 | ``` 79 | 80 | Or _declaratively_ using context-element `data` attribute 81 | ```html 82 | ... 83 | ``` 84 | note: We can only use declarative if the context-element is not a root element 85 | 86 | Inside the context element, we can write a template which is then used to render the data we provide. 87 | To bind the attribute or innerHTML of the template, we can use the `watch` attribute. 88 | 89 | ```html 90 | 91 | 92 | 93 |
94 | 95 | 96 | 97 | 98 |
99 | ``` 100 | 101 | ### Active-Attribute 102 | Context-element templates are html code that has special attributes that we refer to as active-attributes. Active-attribute is an html attribute that has a specific keyword or what we call an active-attribute type (AAT), and each AAT works differently. 103 | At the moment there are 4 AATs supported by context elements: 104 | 1. Watch 105 | 2. Assets 106 | 3. Toggle 107 | 4. Action 108 | 109 | ### watch 110 | Watch, is an Active Attribute Type (AAT) that is used to bind property data, with html elements 111 | 112 | In the following example `` means that the context-element will set the `value` attribute with a value 113 | from the `time` property of the data (`data.time`). 114 | 115 | ### action 116 | Action, is an AAT used to listen to html Event elements. 117 | The html Event is wrapped into an action, then given to the reducer. 118 | Users can implement reducers, to create new data based on old data and action objects. 119 | Html event elements for example are "onclick", "onmouseenter", "onmouseleave", "onsubmit", "oninput" and others. 120 | We can also eliminate the use of prefix on when we declare AAT actions. 121 | 122 | An action object in `context-element` consists of 2 attributes, type and event. 123 | 124 | 1. Action.type is the value that we define in the active-attribute action. 125 | 2. Action.event is the dom event that triggers the action. 126 | 127 | eg : `````` 128 | The action type would be `DO_SOMETHING` and the event would be MouseEvent.click. 129 | 130 | Following is an example of how the `action` attribute is used 131 | ```html 132 | 133 | 134 |
135 |
136 | ``` 137 | 138 | ```javascript 139 | const el = document.getElementById('my-element'); 140 | 141 | el.data = { name : '' }; 142 | 143 | el.reducer = (data,action) => { 144 | const {type,event} = action; 145 | switch (type) { 146 | case 'SET_NAME' : { 147 | const name = event.target.value; 148 | return {...data,name} 149 | } 150 | } 151 | return {...data} 152 | } 153 | ``` 154 | 155 | ## _context-array_ 156 | To render an array, we can use `context-array`. `context-array` is the tag-name of ArrayContextElement class. 157 | ArrayContextElement is a subclass of ContextElement. What distinguishes ArrayContextElement from ContextElement is 158 | type of data. ContextElement can only accept `Object` type data. Whereas ArrayContextElement can only accept 159 | `Array` type data. 160 | 161 | ArrayContextElement requires the `data.key` attribute. The `data.key` attribute must contain the name of the property of the data, which has a unique value. 162 | This data.key will then be used as a marker when there is a new array accepted by the data property, to let ArrayContextElement 163 | to decide whether the active-node should be discarded or updated in the dom. 164 | 165 | ### watch 166 | The following is an example of how we can use the `watch` attribute in ArrayContextElement or` context-array`. 167 | ```html 168 | 169 | 170 | 171 | 184 | ``` 185 | 186 | ### action 187 | 188 | The `action` attribute in` context-array` is slightly different from the action attribute in `context-element`, the action object in 189 | context-array has 4 values: 190 | 1. Action.type: is the value given when we declare the action attribute in the template. 191 | 2. Action.event: is a dom event that triggers action. 192 | 3. Action.data: is a data item from an array. 193 | 4. Action.index: is an index of data items against an array. 194 | 195 | Following is an example of how we can use actions in `context-array` 196 | 197 | ```html 198 | 199 |
200 | 201 |
202 |
203 |
204 | 205 | 222 | ``` 223 | 224 | With the above code we can see that the `SET_CHECKBOX` action will set the value of isChecked with value 225 | the new one from `checkbox.checked` property. 226 | 227 | 228 | ### toggle 229 | Toogle is an active-attribute that is used to toggle the value of the attribute. To use the toggle attribute 230 | we must use the `state` of the context. Consider the following example how toggle used : 231 | 232 | ```html 233 | 234 | 235 | 236 | 255 | ``` 256 | The code above means, if the value of **`data._state`** is` highlight` then the context-element will render the template above to 257 | 258 | ```html 259 | 260 | 261 | 262 | ``` 263 | 264 | ### asset 265 | Assets are active attributes that will bound values from attributes not from data, but from context-element `asset` property. 266 | Following is an example of using Assets. 267 | 268 | ```html 269 | 270 | 271 | 272 | 278 | ``` 279 | Inside the script tag in the sample code above, we assign the value of `assets` to the context-element. `context-element` will bind the value of the placeholder to the value 280 | `Please type your input here?`. 281 | 282 | 283 | In contrast to `data`, if we assign the value of an `asset`, the context-element will not rerendered its content. 284 | 285 | Apart from that, if the context-element cannot find the key of the asset that we are looking for, then the context-element will look to the parent assets. 286 | 287 | #### active-attributes semantic 288 | semantic | meaning 289 | --- | --- | 290 | ```
``` | Bind the data name property into the div content. This is the same as ```
``` 291 | ```
``` | If `data._state` value is `simple`: then bind content div with the property `data.firstName`, if `data._state` value is `detail` : then bind content div with the property` data.fullName`. 292 | `````` | Bind the `data.name` property into the input value. 293 | `````` | If `data._state` value is `people`: then bind the element attribute `input.value` with` data.name`. If `data._state` value is `address`: then bind the element attribute `input.value` with` data.city` 294 | ```
Cloud Strife
``` | If `data._state` has no value, the value of the class attribute is`
Cloud Strife
`, If` data._state` has a value of `disabled` then the value of the class is` < div class = "default disabled"> Cloud Strife
` 295 | 296 | 297 | 298 | ##### Examples & Github Page 299 | head over our [ContextElement page](https://marsa-emreef.github.io/context-element/) to see more context elements in action 300 | 301 | -------------------------------------------------------------------------------- /context-element.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arif-rachim/context-element/9c28388e0d18315ebb9ac05fe6da78b3a170873d/docs/favicon.ico -------------------------------------------------------------------------------- /docs/iframe.html: -------------------------------------------------------------------------------- 1 | Storybook

No Preview

Sorry, but you either have no stories or none are selected somehow.

  • Please check the Storybook config.
  • Try reloading the page.

If the problem persists, check the browser console, or the terminal you've run Storybook from.

-------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | Storybook
-------------------------------------------------------------------------------- /docs/main.3991cff3ec22f54eb0e1.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{529:function(n,o,p){p(530),p(679),p(1414),n.exports=p(1419)},594:function(n,o){},635:function(n,o){}},[[529,1,2]]]); -------------------------------------------------------------------------------- /docs/main.d66ddc34bb951c6d083d.bundle.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[0],{27:function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.d(__webpack_exports__,"a",(function(){return useJavascript}));var useJavascript=function(callback){return requestAnimationFrame(callback)}},354:function(module,exports,__webpack_require__){__webpack_require__(355),__webpack_require__(506),module.exports=__webpack_require__(507)},421:function(module,exports){},462:function(module,exports){},507:function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__),function(module){var _storybook_html__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(353);module._StorybookPreserveDecorators=!0,Object(_storybook_html__WEBPACK_IMPORTED_MODULE_0__.configure)([__webpack_require__(745)],module)}.call(this,__webpack_require__(508)(module))},745:function(module,exports,__webpack_require__){var map={"./stories/context-array/input.stories.ts":746,"./stories/context-array/timer.stories.ts":783,"./stories/context-element/checkbox.stories.ts":803,"./stories/context-element/input.stories.ts":804,"./stories/context-element/timer.stories.ts":805,"./stories/todo.stories.ts":806};function webpackContext(req){var id=webpackContextResolve(req);return __webpack_require__(id)}function webpackContextResolve(req){if(!__webpack_require__.o(map,req)){var e=new Error("Cannot find module '"+req+"'");throw e.code="MODULE_NOT_FOUND",e}return map[req]}webpackContext.keys=function webpackContextKeys(){return Object.keys(map)},webpackContext.resolve=webpackContextResolve,module.exports=webpackContext,webpackContext.id=745},746:function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,"input",(function(){return input}));__webpack_require__(20),__webpack_require__(41),__webpack_require__(42),__webpack_require__(18),__webpack_require__(68),__webpack_require__(21),__webpack_require__(65),__webpack_require__(44),__webpack_require__(32),__webpack_require__(96),__webpack_require__(15),__webpack_require__(67),__webpack_require__(80),__webpack_require__(33),__webpack_require__(136),__webpack_require__(137),__webpack_require__(25),__webpack_require__(19),__webpack_require__(45),__webpack_require__(46),__webpack_require__(22),__webpack_require__(47);var _storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_22__=__webpack_require__(13),_useJavascript__WEBPACK_IMPORTED_MODULE_23__=__webpack_require__(27);function _toConsumableArray(arr){return function _arrayWithoutHoles(arr){if(Array.isArray(arr))return _arrayLikeToArray(arr)}(arr)||function _iterableToArray(iter){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(iter))return Array.from(iter)}(arr)||function _unsupportedIterableToArray(o,minLen){if(!o)return;if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);"Object"===n&&o.constructor&&(n=o.constructor.name);if("Map"===n||"Set"===n)return Array.from(o);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}(arr)||function _nonIterableSpread(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=Array(len);i {\r\n const html = `\r\n

Sample of action in array

\r\n \r\n
Your favorite
\r\n
`;\r\n\r\n useJavascript(() => {\r\n interface Data{\r\n name : string,\r\n id : number\r\n }\r\n const el = document.getElementById(\'myElement\') as ArrayContextElement;\r\n\r\n el.data = object(\'data\',[{\r\n id : 1,\r\n name : \'Javascript\'\r\n },{\r\n id : 2,\r\n name : \'Typescript\'\r\n }]);\r\n\r\n el.reducer = (array,action) => {\r\n let {type,event,index,data} = action;\r\n switch (type) {\r\n case \'SET_FAVORITE\' : {\r\n const newData = {\r\n ...data,\r\n name : (event.target as any).value\r\n }\r\n return [...array.slice(0,index),newData,...array.slice(index+1,array.length)]\r\n }\r\n }\r\n return [...array];\r\n }\r\n });\r\n return html;\r\n}\r\n',locationsMap:{"context-array--input":{startLoc:{col:21,line:8},endLoc:{col:1,line:45},startBody:{col:21,line:8},endBody:{col:1,line:45}}}}},title:"Context Array",decorators:[_storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_22__.withKnobs]};var input=addSourceDecorator((function(){return Object(_useJavascript__WEBPACK_IMPORTED_MODULE_23__.a)((function(){var el=document.getElementById("myElement");el.data=Object(_storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_22__.object)("data",[{id:1,name:"Javascript"},{id:2,name:"Typescript"}]),el.reducer=function(array,action){var type=action.type,event=action.event,index=action.index,data=action.data;switch(type){case"SET_FAVORITE":var newData=_objectSpread(_objectSpread({},data),{},{name:event.target.value});return[].concat(_toConsumableArray(array.slice(0,index)),[newData],_toConsumableArray(array.slice(index+1,array.length)))}return _toConsumableArray(array)}})),'\n

Sample of action in array

\n \n
Your favorite
\n
'}),{__STORY__:'\r\nimport {object, withKnobs} from "@storybook/addon-knobs";\r\nimport {useJavascript} from "../useJavascript";\r\nimport {ArrayContextElement} from "../../array-context-element";\r\n\r\nexport default { title: \'Context Array\',decorators:[withKnobs] };\r\n\r\nexport const input = () => {\r\n const html = `\r\n

Sample of action in array

\r\n \r\n
Your favorite
\r\n
`;\r\n\r\n useJavascript(() => {\r\n interface Data{\r\n name : string,\r\n id : number\r\n }\r\n const el = document.getElementById(\'myElement\') as ArrayContextElement;\r\n\r\n el.data = object(\'data\',[{\r\n id : 1,\r\n name : \'Javascript\'\r\n },{\r\n id : 2,\r\n name : \'Typescript\'\r\n }]);\r\n\r\n el.reducer = (array,action) => {\r\n let {type,event,index,data} = action;\r\n switch (type) {\r\n case \'SET_FAVORITE\' : {\r\n const newData = {\r\n ...data,\r\n name : (event.target as any).value\r\n }\r\n return [...array.slice(0,index),newData,...array.slice(index+1,array.length)]\r\n }\r\n }\r\n return [...array];\r\n }\r\n });\r\n return html;\r\n}\r\n',__ADDS_MAP__:{"context-array--input":{startLoc:{col:21,line:8},endLoc:{col:1,line:45},startBody:{col:21,line:8},endBody:{col:1,line:45}}},__MAIN_FILE_LOCATION__:"/input.stories.ts",__MODULE_DEPENDENCIES__:[],__LOCAL_DEPENDENCIES__:{},__SOURCE_PREFIX__:"C:\\Users\\aarif\\WebstormProjects\\context-element\\src\\stories\\context-array",__IDS_TO_FRAMEWORKS__:{}})},783:function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,"timer",(function(){return timer}));__webpack_require__(784),__webpack_require__(350);var _storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_2__=__webpack_require__(13),_useJavascript__WEBPACK_IMPORTED_MODULE_3__=__webpack_require__(27),addSourceDecorator=(__webpack_require__(26).withSource,__webpack_require__(26).addSource);__webpack_exports__.default={parameters:{storySource:{source:'\r\nimport {object, withKnobs} from "@storybook/addon-knobs";\r\nimport {useJavascript} from "../useJavascript";\r\nimport {ArrayContextElement} from "../../array-context-element";\r\n\r\nexport default { title: \'Context Array\',decorators:[withKnobs] };\r\n\r\nexport const timer = () => {\r\n const html = `\r\n

Generate Random Data

\r\n \r\n
`;\r\n\r\n useJavascript(() => {\r\n interface Data{\r\n time : string,\r\n id : number\r\n }\r\n const el = document.getElementById(\'myElement\') as ArrayContextElement;\r\n\r\n el.data = object(\'data\',[{\r\n id : 1,\r\n time : Math.round(Math.random() * 1000).toFixed()\r\n },{\r\n id : 2,\r\n time : Math.round(Math.random() * 1000).toFixed()\r\n }]);\r\n setInterval(() => {\r\n el.data = [{\r\n id : 1,\r\n time : Math.round(Math.random() * 1000).toFixed()\r\n },{\r\n id : 2,\r\n time : Math.round(Math.random() * 1000).toFixed()\r\n }];\r\n },1000);\r\n });\r\n return html;\r\n}\r\n',locationsMap:{"context-array--timer":{startLoc:{col:21,line:8},endLoc:{col:1,line:39},startBody:{col:21,line:8},endBody:{col:1,line:39}}}}},title:"Context Array",decorators:[_storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_2__.withKnobs]};var timer=addSourceDecorator((function(){return Object(_useJavascript__WEBPACK_IMPORTED_MODULE_3__.a)((function(){var el=document.getElementById("myElement");el.data=Object(_storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_2__.object)("data",[{id:1,time:Math.round(1e3*Math.random()).toFixed()},{id:2,time:Math.round(1e3*Math.random()).toFixed()}]),setInterval((function(){el.data=[{id:1,time:Math.round(1e3*Math.random()).toFixed()},{id:2,time:Math.round(1e3*Math.random()).toFixed()}]}),1e3)})),'\n

Generate Random Data

\n \n
'}),{__STORY__:'\r\nimport {object, withKnobs} from "@storybook/addon-knobs";\r\nimport {useJavascript} from "../useJavascript";\r\nimport {ArrayContextElement} from "../../array-context-element";\r\n\r\nexport default { title: \'Context Array\',decorators:[withKnobs] };\r\n\r\nexport const timer = () => {\r\n const html = `\r\n

Generate Random Data

\r\n \r\n
`;\r\n\r\n useJavascript(() => {\r\n interface Data{\r\n time : string,\r\n id : number\r\n }\r\n const el = document.getElementById(\'myElement\') as ArrayContextElement;\r\n\r\n el.data = object(\'data\',[{\r\n id : 1,\r\n time : Math.round(Math.random() * 1000).toFixed()\r\n },{\r\n id : 2,\r\n time : Math.round(Math.random() * 1000).toFixed()\r\n }]);\r\n setInterval(() => {\r\n el.data = [{\r\n id : 1,\r\n time : Math.round(Math.random() * 1000).toFixed()\r\n },{\r\n id : 2,\r\n time : Math.round(Math.random() * 1000).toFixed()\r\n }];\r\n },1000);\r\n });\r\n return html;\r\n}\r\n',__ADDS_MAP__:{"context-array--timer":{startLoc:{col:21,line:8},endLoc:{col:1,line:39},startBody:{col:21,line:8},endBody:{col:1,line:39}}},__MAIN_FILE_LOCATION__:"/timer.stories.ts",__MODULE_DEPENDENCIES__:[],__LOCAL_DEPENDENCIES__:{},__SOURCE_PREFIX__:"C:\\Users\\aarif\\WebstormProjects\\context-element\\src\\stories\\context-array",__IDS_TO_FRAMEWORKS__:{}})},803:function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,"checkbox",(function(){return checkbox}));__webpack_require__(20),__webpack_require__(68),__webpack_require__(21),__webpack_require__(80),__webpack_require__(33),__webpack_require__(136),__webpack_require__(137),__webpack_require__(25),__webpack_require__(22);var _storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_9__=__webpack_require__(13),_useJavascript__WEBPACK_IMPORTED_MODULE_10__=__webpack_require__(27);function ownKeys(object,enumerableOnly){var keys=Object.keys(object);if(Object.getOwnPropertySymbols){var symbols=Object.getOwnPropertySymbols(object);enumerableOnly&&(symbols=symbols.filter((function(sym){return Object.getOwnPropertyDescriptor(object,sym).enumerable}))),keys.push.apply(keys,symbols)}return keys}function _objectSpread(target){for(var source,i=1;i {\r\n const html = `\r\n \r\n

Value of checkbox is :

\r\n
`;\r\n\r\n useJavascript(() => {\r\n\r\n interface Data{\r\n isDone : boolean\r\n }\r\n\r\n const el = document.getElementById(\'myElement\') as ContextElement;\r\n el.data = object(\'Default State\',{\r\n isDone : false\r\n });\r\n el.reducer = (data,action) => {\r\n const {type,event} = action;\r\n switch (type) {\r\n case \'TOGGLE_CHECKBOX\' : {\r\n const isDone = (event.target as HTMLInputElement).checked;\r\n return {...data,isDone}\r\n }\r\n }\r\n return {...data}\r\n }\r\n });\r\n return html;\r\n}',locationsMap:{"context-element--checkbox":{startLoc:{col:24,line:7},endLoc:{col:1,line:38},startBody:{col:24,line:7},endBody:{col:1,line:38}}}}},title:"Context Element",decorators:[_storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_9__.withKnobs]};var checkbox=addSourceDecorator((function(){return Object(_useJavascript__WEBPACK_IMPORTED_MODULE_10__.a)((function(){var el=document.getElementById("myElement");el.data=Object(_storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_9__.object)("Default State",{isDone:!1}),el.reducer=function(data,action){var type=action.type,event=action.event;switch(type){case"TOGGLE_CHECKBOX":var isDone=event.target.checked;return _objectSpread(_objectSpread({},data),{},{isDone:isDone})}return _objectSpread({},data)}})),'\n \n

Value of checkbox is :

\n
'}),{__STORY__:'import {ContextElement} from "../../context-element";\r\nimport {object, withKnobs} from "@storybook/addon-knobs";\r\nimport {useJavascript} from "../useJavascript";\r\n\r\nexport default { title: \'Context Element\',decorators:[withKnobs] };\r\n\r\nexport const checkbox = () => {\r\n const html = `\r\n \r\n

Value of checkbox is :

\r\n
`;\r\n\r\n useJavascript(() => {\r\n\r\n interface Data{\r\n isDone : boolean\r\n }\r\n\r\n const el = document.getElementById(\'myElement\') as ContextElement;\r\n el.data = object(\'Default State\',{\r\n isDone : false\r\n });\r\n el.reducer = (data,action) => {\r\n const {type,event} = action;\r\n switch (type) {\r\n case \'TOGGLE_CHECKBOX\' : {\r\n const isDone = (event.target as HTMLInputElement).checked;\r\n return {...data,isDone}\r\n }\r\n }\r\n return {...data}\r\n }\r\n });\r\n return html;\r\n}',__ADDS_MAP__:{"context-element--checkbox":{startLoc:{col:24,line:7},endLoc:{col:1,line:38},startBody:{col:24,line:7},endBody:{col:1,line:38}}},__MAIN_FILE_LOCATION__:"/checkbox.stories.ts",__MODULE_DEPENDENCIES__:[],__LOCAL_DEPENDENCIES__:{},__SOURCE_PREFIX__:"C:\\Users\\aarif\\WebstormProjects\\context-element\\src\\stories\\context-element",__IDS_TO_FRAMEWORKS__:{}})},804:function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,"input",(function(){return input}));__webpack_require__(20),__webpack_require__(68),__webpack_require__(21),__webpack_require__(80),__webpack_require__(33),__webpack_require__(136),__webpack_require__(137),__webpack_require__(25),__webpack_require__(22);var _storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_9__=__webpack_require__(13),_useJavascript__WEBPACK_IMPORTED_MODULE_10__=__webpack_require__(27);function ownKeys(object,enumerableOnly){var keys=Object.keys(object);if(Object.getOwnPropertySymbols){var symbols=Object.getOwnPropertySymbols(object);enumerableOnly&&(symbols=symbols.filter((function(sym){return Object.getOwnPropertyDescriptor(object,sym).enumerable}))),keys.push.apply(keys,symbols)}return keys}function _objectSpread(target){for(var source,i=1;i {\r\n const html = `\r\n \r\n \r\n `;\r\n\r\n useJavascript(() => {\r\n interface Data{\r\n name : string\r\n }\r\n const el = document.getElementById(\'myElement\') as ContextElement;\r\n el.data = {\r\n name : \'This is example of binding\'\r\n }\r\n el.reducer = (data,action) => {\r\n const {event,type} = action;\r\n switch (type) {\r\n case \'SET_NAME\' : {\r\n const name = (event.target as HTMLInputElement).value;\r\n return {...data,name}\r\n }\r\n }\r\n return {...data}\r\n }\r\n });\r\n return html;\r\n};\r\n',locationsMap:{"context-element--input":{startLoc:{col:21,line:7},endLoc:{col:1,line:33},startBody:{col:21,line:7},endBody:{col:1,line:33}}}}},title:"Context Element",decorators:[_storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_9__.withKnobs]};var input=addSourceDecorator((function(){return Object(_useJavascript__WEBPACK_IMPORTED_MODULE_10__.a)((function(){var el=document.getElementById("myElement");el.data={name:"This is example of binding"},el.reducer=function(data,action){var event=action.event;switch(action.type){case"SET_NAME":var name=event.target.value;return _objectSpread(_objectSpread({},data),{},{name:name})}return _objectSpread({},data)}})),'\n \n \n '}),{__STORY__:'import {ContextElement} from "../../context-element";\r\nimport {object, withKnobs} from "@storybook/addon-knobs";\r\nimport {useJavascript} from "../useJavascript";\r\n\r\nexport default { title: \'Context Element\',decorators:[withKnobs] };\r\n\r\nexport const input = (args:any) => {\r\n const html = `\r\n \r\n \r\n `;\r\n\r\n useJavascript(() => {\r\n interface Data{\r\n name : string\r\n }\r\n const el = document.getElementById(\'myElement\') as ContextElement;\r\n el.data = {\r\n name : \'This is example of binding\'\r\n }\r\n el.reducer = (data,action) => {\r\n const {event,type} = action;\r\n switch (type) {\r\n case \'SET_NAME\' : {\r\n const name = (event.target as HTMLInputElement).value;\r\n return {...data,name}\r\n }\r\n }\r\n return {...data}\r\n }\r\n });\r\n return html;\r\n};\r\n',__ADDS_MAP__:{"context-element--input":{startLoc:{col:21,line:7},endLoc:{col:1,line:33},startBody:{col:21,line:7},endBody:{col:1,line:33}}},__MAIN_FILE_LOCATION__:"/input.stories.ts",__MODULE_DEPENDENCIES__:[],__LOCAL_DEPENDENCIES__:{},__SOURCE_PREFIX__:"C:\\Users\\aarif\\WebstormProjects\\context-element\\src\\stories\\context-element",__IDS_TO_FRAMEWORKS__:{}})},805:function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,"timer",(function(){return timer}));__webpack_require__(15),__webpack_require__(350);var _storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_2__=__webpack_require__(13),_useJavascript__WEBPACK_IMPORTED_MODULE_3__=__webpack_require__(27),addSourceDecorator=(__webpack_require__(26).withSource,__webpack_require__(26).addSource);__webpack_exports__.default={parameters:{storySource:{source:'import {ContextElement} from "../../context-element";\r\nimport {object, withKnobs} from "@storybook/addon-knobs";\r\nimport {useJavascript} from "../useJavascript";\r\n\r\nexport default { title: \'Context Element\',decorators:[withKnobs] };\r\n\r\nexport const timer = () => {\r\n const html = `\r\n \r\n `;\r\n\r\n useJavascript(() => {\r\n interface Data{\r\n time : string\r\n }\r\n const el = document.getElementById(\'myElement\') as ContextElement;\r\n el.data = object(\'data\',{\r\n time : new Date().toLocaleTimeString()\r\n });\r\n setInterval(() => {\r\n el.data = {\r\n time : new Date().toLocaleTimeString()\r\n }\r\n },1000);\r\n });\r\n return html;\r\n};\r\n',locationsMap:{"context-element--timer":{startLoc:{col:21,line:7},endLoc:{col:1,line:27},startBody:{col:21,line:7},endBody:{col:1,line:27}}}}},title:"Context Element",decorators:[_storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_2__.withKnobs]};var timer=addSourceDecorator((function(){return Object(_useJavascript__WEBPACK_IMPORTED_MODULE_3__.a)((function(){var el=document.getElementById("myElement");el.data=Object(_storybook_addon_knobs__WEBPACK_IMPORTED_MODULE_2__.object)("data",{time:(new Date).toLocaleTimeString()}),setInterval((function(){el.data={time:(new Date).toLocaleTimeString()}}),1e3)})),'\n \n '}),{__STORY__:'import {ContextElement} from "../../context-element";\r\nimport {object, withKnobs} from "@storybook/addon-knobs";\r\nimport {useJavascript} from "../useJavascript";\r\n\r\nexport default { title: \'Context Element\',decorators:[withKnobs] };\r\n\r\nexport const timer = () => {\r\n const html = `\r\n \r\n `;\r\n\r\n useJavascript(() => {\r\n interface Data{\r\n time : string\r\n }\r\n const el = document.getElementById(\'myElement\') as ContextElement;\r\n el.data = object(\'data\',{\r\n time : new Date().toLocaleTimeString()\r\n });\r\n setInterval(() => {\r\n el.data = {\r\n time : new Date().toLocaleTimeString()\r\n }\r\n },1000);\r\n });\r\n return html;\r\n};\r\n',__ADDS_MAP__:{"context-element--timer":{startLoc:{col:21,line:7},endLoc:{col:1,line:27},startBody:{col:21,line:7},endBody:{col:1,line:27}}},__MAIN_FILE_LOCATION__:"/timer.stories.ts",__MODULE_DEPENDENCIES__:[],__LOCAL_DEPENDENCIES__:{},__SOURCE_PREFIX__:"C:\\Users\\aarif\\WebstormProjects\\context-element\\src\\stories\\context-element",__IDS_TO_FRAMEWORKS__:{}})},806:function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.r(__webpack_exports__),__webpack_require__.d(__webpack_exports__,"todo",(function(){return todo}));__webpack_require__(20),__webpack_require__(41),__webpack_require__(42),__webpack_require__(18),__webpack_require__(68),__webpack_require__(21),__webpack_require__(65),__webpack_require__(44),__webpack_require__(32),__webpack_require__(96),__webpack_require__(15),__webpack_require__(67),__webpack_require__(80),__webpack_require__(33),__webpack_require__(136),__webpack_require__(137),__webpack_require__(25),__webpack_require__(19),__webpack_require__(45),__webpack_require__(46),__webpack_require__(22),__webpack_require__(47);var dist=__webpack_require__(13),useJavascript=__webpack_require__(27);__webpack_require__(52),__webpack_require__(193);function uuid(){return"_xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(c){var r=0|16*Math.random();return("x"==c?r:8|3&r).toString(16)}))}function _toConsumableArray(arr){return function _arrayWithoutHoles(arr){if(Array.isArray(arr))return _arrayLikeToArray(arr)}(arr)||function _iterableToArray(iter){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(iter))return Array.from(iter)}(arr)||function _unsupportedIterableToArray(o,minLen){if(!o)return;if("string"==typeof o)return _arrayLikeToArray(o,minLen);var n=Object.prototype.toString.call(o).slice(8,-1);"Object"===n&&o.constructor&&(n=o.constructor.name);if("Map"===n||"Set"===n)return Array.from(o);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return _arrayLikeToArray(o,minLen)}(arr)||function _nonIterableSpread(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function _arrayLikeToArray(arr,len){(null==len||len>arr.length)&&(len=arr.length);for(var i=0,arr2=Array(len);i {\r\n useJavascript(javascript);\r\n return useHtml();\r\n};\r\n\r\n\r\n\r\nconst useHtml = () => `\r\n\r\n\r\n
\r\n \r\n
\r\n \r\n
\r\n \r\n highlight_off\r\n
\r\n \r\n
\r\n
\r\n`;\r\n\r\n\r\n\r\nconst todoItemReducer = (array:any,action:any) => {\r\n const {data,type,event,index} = action;\r\n\r\n switch (type) {\r\n case \'SET_DONE\' : {\r\n const isDone = (event.target as HTMLInputElement).checked;\r\n const newData = {...data,isDone,_state:isDone?\'done\' : \'\'};\r\n return [...array.slice(0,index),newData,...array.slice(index+1,array.length)];\r\n }\r\n case \'DELETE_TODO\' : {\r\n return [...array.filter((item:any,itemIndex:number) => index !== itemIndex)];\r\n }\r\n }\r\n return [...array]\r\n};\r\n\r\nconst mainReducer = (data:any,action:any) => {\r\n const {type,event} = action;\r\n switch (type) {\r\n case \'SET_TODO\' : {\r\n const newTodo = {...data.todo,todo:event.target.value};\r\n return {...data,todo:newTodo}\r\n }\r\n case \'ADD_TODO\' : {\r\n const todo = data.todo;\r\n const newTodo = {id:uuid(),todo:\'\',done:false};\r\n return {...data,todo:newTodo,todos:[...data.todos,todo]}\r\n }\r\n }\r\n return {...data};\r\n};\r\n\r\nconst javascript = () => {\r\n const app = document.getElementById(\'app\') as ContextElement;\r\n let DEFAULT_CONTEXT = {\r\n todo : {\r\n id: uuid(),\r\n todo : \'\',\r\n done : false\r\n },\r\n todos : Array.from([]),\r\n todoItemReducer\r\n };\r\n app.data = DEFAULT_CONTEXT;\r\n app.reducer = mainReducer;\r\n};',locationsMap:{"todo-app--todo":{startLoc:{col:20,line:10},endLoc:{col:1,line:13},startBody:{col:20,line:10},endBody:{col:1,line:13}}}}},title:"Todo App",decorators:[dist.withKnobs]},addSourceDecorator((function(){return Object(useJavascript.a)(javascript),useHtml()}),{__STORY__:'import {ContextElement} from "../context-element";\r\nimport {object, withKnobs} from "@storybook/addon-knobs";\r\nimport {useJavascript} from "./useJavascript";\r\nimport {Reducer} from "../types";\r\nimport uuid from "../libs/uuid";\r\nimport {is} from "@babel/types";\r\n\r\nexport default { title: \'Todo App\',decorators:[withKnobs] };\r\n\r\nexport const todo = () => {\r\n useJavascript(javascript);\r\n return useHtml();\r\n};\r\n\r\n\r\n\r\nconst useHtml = () => `\r\n\r\n\r\n
\r\n \r\n
\r\n \r\n
\r\n \r\n highlight_off\r\n
\r\n \r\n
\r\n
\r\n`;\r\n\r\n\r\n\r\nconst todoItemReducer = (array:any,action:any) => {\r\n const {data,type,event,index} = action;\r\n\r\n switch (type) {\r\n case \'SET_DONE\' : {\r\n const isDone = (event.target as HTMLInputElement).checked;\r\n const newData = {...data,isDone,_state:isDone?\'done\' : \'\'};\r\n return [...array.slice(0,index),newData,...array.slice(index+1,array.length)];\r\n }\r\n case \'DELETE_TODO\' : {\r\n return [...array.filter((item:any,itemIndex:number) => index !== itemIndex)];\r\n }\r\n }\r\n return [...array]\r\n};\r\n\r\nconst mainReducer = (data:any,action:any) => {\r\n const {type,event} = action;\r\n switch (type) {\r\n case \'SET_TODO\' : {\r\n const newTodo = {...data.todo,todo:event.target.value};\r\n return {...data,todo:newTodo}\r\n }\r\n case \'ADD_TODO\' : {\r\n const todo = data.todo;\r\n const newTodo = {id:uuid(),todo:\'\',done:false};\r\n return {...data,todo:newTodo,todos:[...data.todos,todo]}\r\n }\r\n }\r\n return {...data};\r\n};\r\n\r\nconst javascript = () => {\r\n const app = document.getElementById(\'app\') as ContextElement;\r\n let DEFAULT_CONTEXT = {\r\n todo : {\r\n id: uuid(),\r\n todo : \'\',\r\n done : false\r\n },\r\n todos : Array.from([]),\r\n todoItemReducer\r\n };\r\n app.data = DEFAULT_CONTEXT;\r\n app.reducer = mainReducer;\r\n};',__ADDS_MAP__:{"todo-app--todo":{startLoc:{col:20,line:10},endLoc:{col:1,line:13},startBody:{col:20,line:10},endBody:{col:1,line:13}}},__MAIN_FILE_LOCATION__:"/todo.stories.ts",__MODULE_DEPENDENCIES__:[],__LOCAL_DEPENDENCIES__:{},__SOURCE_PREFIX__:"C:\\Users\\aarif\\WebstormProjects\\context-element\\src\\stories",__IDS_TO_FRAMEWORKS__:{}})),useHtml=function(){return'\n\n\n
\n \n
\n \n
\n \n highlight_off\n
\n \n
\n
\n'},todoItemReducer=function(array,action){var data=action.data,type=action.type,event=action.event,index=action.index;switch(type){case"SET_DONE":var isDone=event.target.checked,newData=_objectSpread(_objectSpread({},data),{},{isDone:isDone,_state:isDone?"done":""});return[].concat(_toConsumableArray(array.slice(0,index)),[newData],_toConsumableArray(array.slice(index+1,array.length)));case"DELETE_TODO":return _toConsumableArray(array.filter((function(item,itemIndex){return index!==itemIndex})))}return _toConsumableArray(array)},mainReducer=function(data,action){var type=action.type,event=action.event;switch(type){case"SET_TODO":var newTodo=_objectSpread(_objectSpread({},data.todo),{},{todo:event.target.value});return _objectSpread(_objectSpread({},data),{},{todo:newTodo});case"ADD_TODO":var _todo=data.todo,_newTodo={id:uuid(),todo:"",done:!1};return _objectSpread(_objectSpread({},data),{},{todo:_newTodo,todos:[].concat(_toConsumableArray(data.todos),[_todo])})}return _objectSpread({},data)},javascript=function(){var app=document.getElementById("app"),DEFAULT_CONTEXT={todo:{id:uuid(),todo:"",done:!1},todos:Array.from([]),todoItemReducer:todoItemReducer};app.data=DEFAULT_CONTEXT,app.reducer=mainReducer}}},[[354,1,2]]]); 2 | //# sourceMappingURL=main.d66ddc34bb951c6d083d.bundle.js.map -------------------------------------------------------------------------------- /docs/main.d66ddc34bb951c6d083d.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"main.d66ddc34bb951c6d083d.bundle.js","sources":["webpack:///main.d66ddc34bb951c6d083d.bundle.js"],"mappings":"AAAA","sourceRoot":""} -------------------------------------------------------------------------------- /docs/runtime~main.286b69a873d49f2f199c.bundle.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,f=r[0],i=r[1],a=r[2],c=0,s=[];c 40 | * 41 | * Copyright (c) 2014-2017, Jon Schlinkert. 42 | * Released under the MIT License. 43 | */ 44 | 45 | /** 46 | * @license 47 | * Lodash 48 | * Copyright OpenJS Foundation and other contributors 49 | * Released under MIT license 50 | * Based on Underscore.js 1.8.3 51 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 52 | */ 53 | 54 | /** @license React v0.18.0 55 | * scheduler.production.min.js 56 | * 57 | * Copyright (c) Facebook, Inc. and its affiliates. 58 | * 59 | * This source code is licensed under the MIT license found in the 60 | * LICENSE file in the root directory of this source tree. 61 | */ 62 | 63 | /** @license React v16.12.0 64 | * react-dom.production.min.js 65 | * 66 | * Copyright (c) Facebook, Inc. and its affiliates. 67 | * 68 | * This source code is licensed under the MIT license found in the 69 | * LICENSE file in the root directory of this source tree. 70 | */ 71 | 72 | /** @license React v16.12.0 73 | * react-is.production.min.js 74 | * 75 | * Copyright (c) Facebook, Inc. and its affiliates. 76 | * 77 | * This source code is licensed under the MIT license found in the 78 | * LICENSE file in the root directory of this source tree. 79 | */ 80 | 81 | /** @license React v16.12.0 82 | * react.production.min.js 83 | * 84 | * Copyright (c) Facebook, Inc. and its affiliates. 85 | * 86 | * This source code is licensed under the MIT license found in the 87 | * LICENSE file in the root directory of this source tree. 88 | */ 89 | 90 | /**! 91 | * @fileOverview Kickass library to create and place poppers near their reference elements. 92 | * @version 1.16.1 93 | * @license 94 | * Copyright (c) 2016 Federico Zivolo and contributors 95 | * 96 | * Permission is hereby granted, free of charge, to any person obtaining a copy 97 | * of this software and associated documentation files (the "Software"), to deal 98 | * in the Software without restriction, including without limitation the rights 99 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 100 | * copies of the Software, and to permit persons to whom the Software is 101 | * furnished to do so, subject to the following conditions: 102 | * 103 | * The above copyright notice and this permission notice shall be included in all 104 | * copies or substantial portions of the Software. 105 | * 106 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 107 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 108 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 109 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 110 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 111 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 112 | * SOFTWARE. 113 | */ 114 | -------------------------------------------------------------------------------- /docs/vendors~main.d66ddc34bb951c6d083d.bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | * escape-html 9 | * Copyright(c) 2012-2013 TJ Holowaychuk 10 | * Copyright(c) 2015 Andreas Lubbe 11 | * Copyright(c) 2015 Tiancheng "Timothy" Gu 12 | * MIT Licensed 13 | */ 14 | 15 | /*! 16 | * https://github.com/es-shims/es5-shim 17 | * @license es5-shim Copyright 2009-2020 by contributors, MIT License 18 | * see https://github.com/es-shims/es5-shim/blob/master/LICENSE 19 | */ 20 | 21 | /*! 22 | * https://github.com/paulmillr/es6-shim 23 | * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com) 24 | * and contributors, MIT License 25 | * es6-shim: v0.35.4 26 | * see https://github.com/paulmillr/es6-shim/blob/0.35.3/LICENSE 27 | * Details and documentation: 28 | * https://github.com/paulmillr/es6-shim/ 29 | */ 30 | 31 | /*! 32 | * is-plain-object 33 | * 34 | * Copyright (c) 2014-2017, Jon Schlinkert. 35 | * Released under the MIT License. 36 | */ 37 | 38 | /*! 39 | * isobject 40 | * 41 | * Copyright (c) 2014-2017, Jon Schlinkert. 42 | * Released under the MIT License. 43 | */ 44 | 45 | /** @license React v0.19.1 46 | * scheduler.production.min.js 47 | * 48 | * Copyright (c) Facebook, Inc. and its affiliates. 49 | * 50 | * This source code is licensed under the MIT license found in the 51 | * LICENSE file in the root directory of this source tree. 52 | */ 53 | 54 | /** @license React v16.13.1 55 | * react-dom.production.min.js 56 | * 57 | * Copyright (c) Facebook, Inc. and its affiliates. 58 | * 59 | * This source code is licensed under the MIT license found in the 60 | * LICENSE file in the root directory of this source tree. 61 | */ 62 | 63 | /** @license React v16.13.1 64 | * react.production.min.js 65 | * 66 | * Copyright (c) Facebook, Inc. and its affiliates. 67 | * 68 | * This source code is licensed under the MIT license found in the 69 | * LICENSE file in the root directory of this source tree. 70 | */ 71 | 72 | //! stable.js 0.1.8, https://github.com/Two-Screen/stable 73 | 74 | //! © 2018 Angry Bytes and contributors. MIT licensed. 75 | -------------------------------------------------------------------------------- /docs/vendors~main.d66ddc34bb951c6d083d.bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"vendors~main.d66ddc34bb951c6d083d.bundle.js","sources":["webpack:///vendors~main.d66ddc34bb951c6d083d.bundle.js"],"mappings":";AAAA","sourceRoot":""} -------------------------------------------------------------------------------- /example/action/input-change.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 29 | 30 | -------------------------------------------------------------------------------- /example/array/cities.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 |
14 | 27 | 28 | -------------------------------------------------------------------------------- /example/color-palette/index.css: -------------------------------------------------------------------------------- 1 | body{ 2 | margin: 0px; 3 | padding: 0px; 4 | } 5 | .container{ 6 | display: flex; 7 | width: 100vw; 8 | height : 100vh; 9 | align-items: center; 10 | justify-content: center; 11 | } 12 | .box{ 13 | background-color: rgba(0,0,0,0); 14 | display: inline-block; 15 | border: 10rem solid rgba(0,0,0,0); 16 | border-bottom:77rem solid rgba(255,0,0,1); 17 | transform: rotate(0deg); 18 | transform-origin: top; 19 | } 20 | .box-container{ 21 | margin-top: -10rem; 22 | } 23 | .dot{ 24 | width: 2px; 25 | height: 2px; 26 | display: flex; 27 | justify-content: center; 28 | transform: rotate(10deg); 29 | position: absolute; 30 | } 31 | .context-array{ 32 | width: 40rem; 33 | height: 40rem; 34 | border-radius: 40rem; 35 | overflow: hidden; 36 | display: flex; 37 | align-items: center; 38 | justify-content: center; 39 | position: absolute; 40 | 41 | } 42 | .palates-container{ 43 | position: relative; 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | } 48 | 49 | .input-position{ 50 | z-index: 99; 51 | position: absolute; 52 | top:1rem; 53 | right:1rem; 54 | border: none; 55 | font-size: 2rem; 56 | outline: none; 57 | padding : 1rem; 58 | width :280px; 59 | text-align: center; 60 | } -------------------------------------------------------------------------------- /example/color-palette/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Color Palete 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /example/color-palette/index.js: -------------------------------------------------------------------------------- 1 | document.body.onload = () => { 2 | const palateElement = document.getElementById('palate'); 3 | const layer = 16; 4 | const maximumHueColor = 360; 5 | const palate = 24; 6 | const degree = Math.round(maximumHueColor / palate ); 7 | const saturationRange = 50; 8 | const lightRange = 60; 9 | palateElement.data = { 10 | input : { 11 | style : '', 12 | color : '' 13 | }, 14 | palateOne : Array.from({length:layer}).map((_,id) => { 15 | const layerIndex = id; 16 | const saturation = Math.round((saturationRange / layer) * (layer - (layerIndex + 0))); 17 | const scale = `transform: scale(${ (1/layer) * (layer - layerIndex) });`; 18 | const light = Math.round((lightRange / layer) * (layer - layerIndex)); 19 | return { 20 | id, 21 | scale, 22 | palateTwo : Array.from({length:(palate)}).map((_,id) => { 23 | const color = `hsl(${degree * id},${(100 - saturationRange)+saturation}%,${light + 20 }%)`; 24 | const colorStyle = `border-bottom-color: ${color} !important;`; 25 | return { 26 | id, 27 | style:`transform: rotate(${(degree) * id}deg);`, 28 | colorStyle, 29 | color 30 | } 31 | }) 32 | } 33 | }) 34 | }; 35 | 36 | palateElement.reducer = (context,action) => { 37 | const {type,event,childActions} = action; 38 | const [levelOne,levelTwo] = childActions; 39 | 40 | switch (type) { 41 | case 'SET_COLOR' : { 42 | return {...context,input:{ 43 | style : `background-color : ${levelTwo.data.color}`, 44 | color : levelTwo.data.color 45 | }} 46 | } 47 | } 48 | return {...context}; 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /example/nested-element/nested-element.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 |
15 |
16 | 36 | 37 | -------------------------------------------------------------------------------- /example/todo/index.ts: -------------------------------------------------------------------------------- 1 | import {Action, ArrayAction, Reducer} from "../../src/types"; 2 | import {ContextElement} from "../../src/context-element"; 3 | import uuid from "../../src/libs/uuid"; 4 | 5 | 6 | const todo = document.getElementById('myTodo') as ContextElement; 7 | 8 | type Todo = { 9 | id: string, 10 | todo: string, 11 | done: boolean, 12 | _state?: string 13 | }; 14 | 15 | type Context = { 16 | todo?: Todo, 17 | todoCollection: Todo[], 18 | todoReducer: Reducer 19 | } 20 | 21 | const DEFAULT_STATE: Context = { 22 | todo: { 23 | done: false, 24 | todo: '', 25 | id: uuid() 26 | }, 27 | todoCollection: [], 28 | todoReducer: (context: Todo[], action: ArrayAction) => { 29 | debugger; 30 | switch (action.type) { 31 | case 'TOGGLE_CHECKBOX' : { 32 | return [...context.slice(0, action.index), { 33 | ...action.data, 34 | done: (action.event.target as HTMLInputElement).checked 35 | }, ...context.slice(action.index + 1, context.length)]; 36 | } 37 | } 38 | return [...context]; 39 | } 40 | }; 41 | 42 | todo.reducer = (context: Context, action: ArrayAction) => { 43 | switch (action.type) { 44 | case 'SET_TODO' : { 45 | const todo = {...context.todo}; 46 | todo.todo = (action.event.target as HTMLInputElement).value; 47 | return {...context, todo}; 48 | } 49 | case 'ADD_TODO' : { 50 | const newContext: Context = {...context, todoCollection: [...context.todoCollection, context.todo]}; 51 | newContext.todo = {id: uuid(), todo: '', done: false}; 52 | return newContext; 53 | } 54 | } 55 | return {...context}; 56 | }; 57 | 58 | todo.data = DEFAULT_STATE; 59 | -------------------------------------------------------------------------------- /example/todo/todo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example of TODO HTML 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /example/watch/time.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Current Time is :
8 |
9 |
10 | 18 | 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | const composeChangeEventName = (attribute) => `${attribute}Changed`; 5 | const hasValue = (param) => param !== undefined && param !== null && param !== ''; 6 | const hasNoValue = (param) => !hasValue(param); 7 | const contains = (text, texts) => texts.reduce((acc, txt) => acc || text.indexOf(txt) >= 0, false); 8 | const DATA_WATCH_ATTRIBUTE = 'watch'; 9 | const DATA_ACTION_ATTRIBUTE = 'action'; 10 | const DATA_ASSET_ATTRIBUTE = 'asset'; 11 | const DATA_KEY_ATTRIBUTE = 'data.key'; 12 | const HIDE_CLASS = "data-element-hidden"; 13 | const ARRAY_CONTEXT_ELEMENT_TAG_NAME = 'context-array'; 14 | const CONTEXT_ELEMENT_TAG_NAME = 'context-element'; 15 | const CHILD_ACTION_EVENT = 'childAction'; 16 | const style = document.createElement('style'); 17 | style.innerHTML = `.${HIDE_CLASS} {display: none !important;}`; 18 | document.head.appendChild(style); 19 | 20 | /** 21 | * Function to remove empty text node. 22 | */ 23 | function noEmptyTextNode() { 24 | return (node) => { 25 | if (node.nodeType === Node.TEXT_NODE) { 26 | return /\S/.test(node.textContent); 27 | } 28 | return true; 29 | }; 30 | } 31 | 32 | const ignoredAttributes = ['data', 'reducer']; 33 | /** 34 | * isValidAttribute return if there is active-attribute to be ignore by the ContextElement. 35 | * @param attributeName 36 | */ 37 | function isValidAttribute(attributeName) { 38 | return ignoredAttributes.indexOf(attributeName) < 0; 39 | } 40 | 41 | /** 42 | * AttributeEvaluator is a class that stores information about node that have active-attributes. 43 | * The AttributeEvaluator is called by the DataRenderer object when DataRenderer.render is executed. 44 | * 45 | * AttributeEvaluator require the activeNode,dataGetter,updateDataCallback, and the reducer function from the DataRenderer. 46 | * 47 | * When the AttributeEvaluator initiated, the attribute evaluator will extract all the active-attributes from active-node and store them in 48 | * `activeAttributeValue`. 49 | * 50 | * Once the activeAttribute extracted from the node, AttributeEvaluator will remove those attributes from the node, remaining 51 | * only non-active attributes. 52 | * 53 | * The non active attributes then will be extracted from the node, and stored in the `defaultAttributeValue` property. 54 | * 55 | * The next step of the initialization process it to extract the active attributes and group them into 3 different map. 56 | * 1. stateAttributeProperty : mapping of `data.property` group by first state then attribute. 57 | * 2. attributeStateProperty : mapping of `data.property` group by first attribute then state. 58 | * 3. eventStateAction : mapping of action group by first event then state. 59 | * 60 | * The last step of the initialization of AttributeEvaluator, is to bind the node against eventStateAction. 61 | */ 62 | class AttributeEvaluator { 63 | /** 64 | * Constructor will perform initialization by constructing activeAttributeValue, defaultAttributeValue, eventStateAction, 65 | * stateAttributeProperty and attributeStateProperty. 66 | * The last process would be initialization of event listener. 67 | * 68 | * @param activeNode : node that contains active-attribute. 69 | * @param assetGetter : callback function to get the asset from context-data 70 | * @param dataGetter : callback function to return current data. 71 | * @param updateData : callback function to inform DataRenderer that a new data is created because of user action. 72 | * @param reducerGetter : function to map data into a new one because of user action. 73 | * @param activeAttributes : attributes that is used to lookup the nodes 74 | * @param bubbleChildAction 75 | */ 76 | constructor(activeNode, assetGetter, dataGetter, updateData, reducerGetter, activeAttributes, bubbleChildAction) { 77 | // mapping for watch & assets 78 | this.attributeProperty = null; 79 | // mapping for action 80 | this.eventAction = null; 81 | /** 82 | * Render method will be invoked my DataRenderer.render. Render method will perform 2 major things, 83 | * update active-attribute `watch:updateAttributeWatch` and `toggle:updateToggleAttribute`. 84 | */ 85 | this.render = () => { 86 | const element = this.activeNode; 87 | const stateAttributeProperty = this.attributeProperty; 88 | const dataGetterValue = this.dataGetter(); 89 | const data = dataGetterValue.data; 90 | const assetGetter = this.assetGetter; 91 | updateWatchAttribute(element, stateAttributeProperty, data, assetGetter); 92 | }; 93 | this.activeNode = activeNode; 94 | this.dataGetter = dataGetter; 95 | this.assetGetter = assetGetter; 96 | this.updateData = updateData; 97 | this.bubbleChildAction = bubbleChildAction; 98 | this.reducerGetter = reducerGetter; 99 | this.activeAttributeValue = populateActiveAttributeValue(activeNode, activeAttributes); 100 | this.eventAction = mapEventStateAction(this.activeAttributeValue); 101 | this.attributeProperty = mapAttributeProperty(this.activeAttributeValue, [DATA_WATCH_ATTRIBUTE, DATA_ASSET_ATTRIBUTE]); 102 | initEventListener(activeNode, this.eventAction, dataGetter, updateData, reducerGetter, bubbleChildAction); 103 | } 104 | } 105 | /** 106 | * mapEventStateAction is a function to convert `action` active-attribute to action group by first event, then state. 107 | * @param attributeValue is an active attribute 108 | */ 109 | const mapEventStateAction = (attributeValue) => { 110 | const eventStateAction = new Map(); 111 | attributeValue.forEach((value, attributeName) => { 112 | if (attributeName.endsWith(DATA_ACTION_ATTRIBUTE)) { 113 | const attributes = attributeName.split('.'); 114 | let event = ''; 115 | if (attributes.length === 1) { 116 | event = 'click'; 117 | } 118 | else { 119 | event = attributes[0]; 120 | } 121 | eventStateAction.set(event, value); 122 | } 123 | }); 124 | return eventStateAction; 125 | }; 126 | /** 127 | * mapStateAttributeProperty is a function to convert `watch` active-attribute to property group by first state, then attribute. 128 | * @param attributeValue 129 | * @param attributePrefixes 130 | */ 131 | const mapAttributeProperty = (attributeValue, attributePrefixes) => { 132 | const attributeProperty = new Map(); 133 | attributeValue.forEach((value, attributeName) => { 134 | if (attributePrefixes.filter(attributePrefix => attributeName.endsWith(attributePrefix)).length > 0) { 135 | const attributes = attributeName.split('.'); 136 | let attribute = ''; 137 | let type = ''; 138 | if (attributes.length === 1) { 139 | attribute = 'content'; 140 | type = attributes[0]; 141 | } 142 | else { 143 | attribute = attributes[0]; 144 | type = attributes[1]; 145 | } 146 | if (!attributeProperty.has(attribute)) { 147 | attributeProperty.set(attribute, new Map()); 148 | } 149 | attributeProperty.get(attribute).set(type, value); 150 | } 151 | }); 152 | return attributeProperty; 153 | }; 154 | /** 155 | * populateActiveAttributeValue will extract the active-attributes from the element. 156 | * @param element 157 | * @param activeAttributes 158 | */ 159 | const populateActiveAttributeValue = (element, activeAttributes) => { 160 | const attributeValue = new Map(); 161 | element.getAttributeNames().filter(name => contains(name, activeAttributes)).forEach(attributeName => { 162 | attributeValue.set(attributeName, element.getAttribute(attributeName)); 163 | element.removeAttribute(attributeName); 164 | }); 165 | return attributeValue; 166 | }; 167 | /** 168 | * InitEventListener is the function to attach `toggle` active-attribute to HTMLElement.addEventListener. 169 | * This method requires htmlElement, eventStateAction, dataGetter, updateData and reducer. 170 | * 171 | * initEventListener will iterate over the eventStateAction map. Based on the event in eventStateAction, the function 172 | * will addEventListener to the element. 173 | * 174 | * When an event is triggered by the element, the eventListener callback will check event.type. 175 | * If the event.type is `submit` then the event will be prevented and propagation stopped. 176 | * 177 | * When element triggered an event, the current data.state will be verified against the eventStateAction. 178 | * If the current data.state is not available in the eventStateAction, then the event will be ignored. 179 | * 180 | * If the current data.state is available in the eventStateAction, or when GlobalState exist in the eventStateAction, then the 181 | * updateData callback will invoked to inform DataRenderer that user is triggering an action. 182 | * 183 | * @param element 184 | * @param eventStateAction 185 | * @param dataGetter 186 | * @param updateData 187 | * @param reducerGetter 188 | * @param bubbleChildAction 189 | */ 190 | const initEventListener = (element, eventStateAction, dataGetter, updateData, reducerGetter, bubbleChildAction) => { 191 | eventStateAction.forEach((stateAction, event) => { 192 | event = event.startsWith('on') ? event.substring('on'.length, event.length) : event; 193 | element.addEventListener(event, (event) => { 194 | event.preventDefault(); 195 | event.stopImmediatePropagation(); 196 | const dataGetterValue = dataGetter(); 197 | const reducer = reducerGetter(); 198 | const type = stateAction; 199 | let data = dataGetterValue.data; 200 | const action = { type, event }; 201 | if ('key' in dataGetterValue) { 202 | const arrayDataGetterValue = dataGetterValue; 203 | data = arrayDataGetterValue.data; 204 | action.data = data; 205 | action.key = arrayDataGetterValue.key; 206 | action.index = arrayDataGetterValue.index; 207 | } 208 | if (hasNoValue(reducer)) { 209 | bubbleChildAction(action); 210 | } 211 | else { 212 | updateData((oldData) => reducer(oldData, action)); 213 | } 214 | }); 215 | }); 216 | }; 217 | /** 218 | * Function to set property of an element, it will check if the attribute is a valid attribute, if its a valid attribute 219 | * then it will set the attribute value, and if the attribute is element property, then the element will be assigned for the attribute. 220 | * 221 | * @param attribute 222 | * @param element 223 | * @param val 224 | * @param data 225 | * @param property 226 | */ 227 | function setPropertyValue(attribute, element, val, data, property) { 228 | if (isValidAttribute(attribute) && element.getAttribute(attribute) !== val) { 229 | element.setAttribute(attribute, val); 230 | } 231 | if (attribute in element) { 232 | element[attribute] = val; 233 | if (attribute === 'data') { 234 | element.dataPath = property; 235 | } 236 | const eventName = composeChangeEventName(attribute); 237 | element[eventName] = (val) => injectValue(data, property, val); 238 | } 239 | if (attribute === 'content') { 240 | element.innerHTML = val; 241 | } 242 | } 243 | /** 244 | * UpdateWatchAttribute is a method that will perform update against `watch` active-attribute. 245 | * 246 | * UpdateWatchAttribute will get the current attributeProps from stateAttributeProps based on the data.state. 247 | * It will iterate over the attribute from the attributeProps. 248 | * On each attribute iteration, the method will set the element.attribute based on the value of data[property]. 249 | * 250 | * If the attribute is also a valid element.property, it will set the value of element.property against the 251 | * data[property] value either. 252 | * 253 | * If the attribute value is `content`, the element.innerHTML value will be set against the data[property] value. 254 | * 255 | * @param element : node or also an HTMLElement 256 | * @param stateAttributeProperty : object that store the mapping of property against state and attribute. 257 | * @param data : current value of the data. 258 | * @param dataState : state value of the object. 259 | * @param assetGetter : callback to get the asset of the context element. 260 | */ 261 | const updateWatchAttribute = (element, stateAttributeProperty, data, assetGetter) => { 262 | const attributeProps = stateAttributeProperty; 263 | if (hasNoValue(attributeProps)) { 264 | return; 265 | } 266 | attributeProps.forEach((typeProperty, attribute) => { 267 | const watchProperty = typeProperty.get(DATA_WATCH_ATTRIBUTE); 268 | const assetProperty = typeProperty.get(DATA_ASSET_ATTRIBUTE); 269 | let val = null; 270 | if (hasValue(watchProperty)) { 271 | val = extractValue(data, watchProperty); 272 | } 273 | else if (hasValue(assetProperty)) { 274 | val = assetGetter(assetProperty); 275 | } 276 | setPropertyValue(attribute, element, val, data, watchProperty); 277 | }); 278 | }; 279 | /** 280 | * Function to extract the value of json from jsonPath 281 | * @param data 282 | * @param prop 283 | */ 284 | const extractValue = (data, prop) => { 285 | if (hasNoValue(data)) { 286 | return data; 287 | } 288 | try { 289 | const evaluate = new Function('data', `return data.${prop};`); 290 | return evaluate.call(null, data); 291 | } 292 | catch (err) { 293 | console.warn(data, err.message); 294 | } 295 | return null; 296 | }; 297 | /** 298 | * Function to extract the value of json from jsonPath 299 | * @param data 300 | * @param prop 301 | * @param value 302 | * 303 | */ 304 | const injectValue = (data, prop, value) => { 305 | if (hasNoValue(data)) { 306 | return; 307 | } 308 | try { 309 | const evaluate = new Function('data', 'value', `data.${prop} = value;`); 310 | return evaluate.call(null, data, value); 311 | } 312 | catch (err) { 313 | console.warn(err.message); 314 | } 315 | }; 316 | 317 | /** 318 | * DataRenderer is an object that store cloned ContextElement.template and store it in 'nodes' property. 319 | * During initialization, DataRenderer scanned for the active-nodes against nodes property. 320 | * active-nodes are the node that contain active-attributes such as `watch|toggle|action`. 321 | * 322 | * When the active nodes identified, DataRenderer create AttributeEvaluator against each active-node, and store them in 323 | * attributeEvaluators property. 324 | * 325 | * When DataRenderer.render invoked by the ContextElement, DataRenderer iterate all ActiveAttributes and call 326 | * ActiveAttribute.render method. 327 | */ 328 | class DataRenderer { 329 | /** 330 | * Constructor to setup the DataRenderer initialization. 331 | * 332 | * @param nodes is a cloned of ContextElement.template 333 | * @param assetGetter 334 | * @param updateData 335 | * @param reducerGetter 336 | * @param bubbleChildAction 337 | * @param updateDataFromChild 338 | */ 339 | constructor(nodes, assetGetter, updateData, reducerGetter, bubbleChildAction, updateDataFromChild) { 340 | /** 341 | * Render with iterate all the AttributeEvaluators and call the AttributeEvaluator.render 342 | * @param getter 343 | */ 344 | this.render = (getter) => { 345 | this.dataGetter = getter; 346 | this.attributeEvaluators.forEach((attributeEvaluator) => attributeEvaluator.render()); 347 | }; 348 | this.nodes = nodes; 349 | this.addChildActionEventListener(updateDataFromChild); 350 | const activeAttributes = [DATA_WATCH_ATTRIBUTE, DATA_ACTION_ATTRIBUTE, DATA_ASSET_ATTRIBUTE]; 351 | const activeNodes = Array.from(activeNodesLookup(activeAttributes, nodes)); 352 | const dataGetter = () => this.dataGetter(); 353 | this.attributeEvaluators = activeNodes.map(activeNode => new AttributeEvaluator(activeNode, assetGetter, dataGetter, updateData, reducerGetter, activeAttributes, bubbleChildAction)); 354 | } 355 | addChildActionEventListener(updateDataFromChild) { 356 | this.nodes.forEach((node) => { 357 | node.addEventListener(CHILD_ACTION_EVENT, (event) => { 358 | if (event.defaultPrevented) { 359 | return; 360 | } 361 | event.stopImmediatePropagation(); 362 | event.stopPropagation(); 363 | event.preventDefault(); 364 | const childAction = event.detail; 365 | const currentData = this.dataGetter(); 366 | const currentAction = { 367 | index: currentData.index, 368 | event: childAction.event, 369 | type: childAction.type, 370 | data: currentData.data, 371 | key: currentData.key 372 | }; 373 | updateDataFromChild(childAction, currentAction); 374 | }); 375 | }); 376 | } 377 | } 378 | /** 379 | * activeNodesLookup will return nodes which has the `active-attributes`. Active attributes are the node attribute that contains attributesSuffix. 380 | * Example of active-attributes value.watch . 381 | *
382 |      *     
383 |      *         
384 | * 385 | *
386 | *
387 | *
388 | * @param attributesSuffix watch|toggle|action 389 | * @param nodes filter 390 | */ 391 | const activeNodesLookup = (attributesSuffix, nodes) => { 392 | return nodes.filter(noEmptyTextNode()).reduce((accumulator, node) => { 393 | if (!(node instanceof HTMLElement)) { 394 | return accumulator; 395 | } 396 | const element = node; 397 | const attributeNames = element.getAttributeNames(); 398 | for (const attribute of attributeNames) { 399 | if (contains(attribute, attributesSuffix)) { 400 | accumulator.add(element); 401 | } 402 | } 403 | if (!contains(element.tagName, [ARRAY_CONTEXT_ELEMENT_TAG_NAME.toUpperCase(), CONTEXT_ELEMENT_TAG_NAME.toUpperCase()])) { 404 | const childrenNodes = activeNodesLookup(attributesSuffix, Array.from(element.childNodes)); 405 | Array.from(childrenNodes).forEach(childNode => accumulator.add(childNode)); 406 | } 407 | return accumulator; 408 | }, new Set()); 409 | }; 410 | 411 | /** 412 | * ContextElement is HTMLElement which can render data in accordance with the template defined in it. 413 | * The following is an example of how we display the template page. 414 | * 415 | *
416 |      *     
417 |      *         
418 |      *             
419 | *
420 | *
421 | *
422 | * 426 | *
427 | *
428 | * 429 | * ContextElement will populate the data into template by looking at the attribute which has watch keyword in it. 430 | * These attribute which has keyword `watch` in it are also known as active-attribute. 431 | * There are 4 kinds of active-attribute, (watch / toggle / action / assets). each attribute works with a different mechanism when ContextElement renders the data. 432 | * 433 | */ 434 | class ContextElement extends HTMLElement { 435 | /** 436 | * Constructor sets default value of reducer to return the parameter immediately (param) => param. 437 | */ 438 | constructor() { 439 | super(); 440 | /** 441 | * Callback function to set the data, 442 | *
443 |              *     
444 |              *         contextElement.setData(data => ({...data,attribute:newValue});
445 |              *     
446 |              * 
447 | * 448 | * @param context 449 | */ 450 | this.setData = (context) => { 451 | this.contextData = context(this.contextData); 452 | this.render(); 453 | }; 454 | /** 455 | * onMounted is invoke when the Element is ready and mounted to the window.document. 456 | *
457 |              *     
458 |              *         contextElement.onMounted(() => console.log(`ChildNodes Ready `,contextElement.childNodes.length > 0));
459 |              *     
460 |              * 
461 | * @param onMountedListener 462 | */ 463 | this.onMounted = (onMountedListener) => { 464 | this.onMountedCallback = onMountedListener; 465 | }; 466 | /** 467 | * Get the assets from the current assets or the parent context element assets. 468 | * @param key 469 | */ 470 | this.getAsset = (key) => { 471 | const assets = this.assets; 472 | if (hasValue(assets) && key in assets) { 473 | return assets[key]; 474 | } 475 | const superContextElement = this.superContextElement; 476 | if (hasValue(superContextElement)) { 477 | return superContextElement.getAsset(key); 478 | } 479 | return null; 480 | }; 481 | /** 482 | * Convert action to ActionPath 483 | * @param arrayAction 484 | */ 485 | this.actionToPath = (arrayAction) => { 486 | const actionPath = { path: this.dataPath }; 487 | if (hasValue(arrayAction.key)) { 488 | actionPath.key = arrayAction.key; 489 | actionPath.index = arrayAction.index; 490 | actionPath.data = arrayAction.data; 491 | } 492 | return actionPath; 493 | }; 494 | /** 495 | * updateDataCallback is a callback function that will set the data and call `dataChanged` method. 496 | *
497 |              *     
498 |              *         contextElement.dataChanged = (data) => console.log("data changed");
499 |              *     
500 |              * 
501 | * @param dataSetter 502 | */ 503 | this.updateDataCallback = (dataSetter) => { 504 | this.setData(dataSetter); 505 | const dataChangedEvent = composeChangeEventName('data'); 506 | if (dataChangedEvent in this) { 507 | this[dataChangedEvent].call(this, this.contextData); 508 | } 509 | }; 510 | /** 511 | * To bubble child action to the parent. 512 | * @param action 513 | */ 514 | this.bubbleChildAction = (action) => { 515 | const childAction = { 516 | event: action.event, 517 | type: action.type, 518 | childActions: [this.actionToPath(action)] 519 | }; 520 | this.dispatchDetailEvent(childAction); 521 | }; 522 | /** 523 | * Updating current data from child action 524 | * @param action 525 | * @param currentAction 526 | */ 527 | this.updateDataFromChild = (action, currentAction) => { 528 | const reducer = this.reducer; 529 | if (hasNoValue(reducer)) { 530 | action.childActions = [this.actionToPath(currentAction), ...action.childActions]; 531 | this.dispatchDetailEvent(action); 532 | } 533 | else { 534 | this.updateDataCallback((oldData) => { 535 | return reducer(oldData, action); 536 | }); 537 | } 538 | }; 539 | /** 540 | * render method is invoked by the component when it received a new data-update. 541 | * First it will create DataRenderer object if its not exist. 542 | * DataRenderer require ContextElement cloned template , updateDataCallback, and reducer. 543 | * 544 | * `cloned template` will be used by the DataRenderer as the real node that will be attached to document body. 545 | * `updateDataCallback` will be used by the DataRenderer to inform the ContextElement if there's new data-update performed by user action. 546 | * `reducer` is an function that will return a new copy of the data.Reducer is invoked when there's user action/ 547 | * 548 | * Each time render method is invoked, a new callback to get the latest data (dataGetter) is created and passed to 549 | * DataRenderer render method. 550 | * 551 | */ 552 | this.render = () => { 553 | if (hasNoValue(this.contextData) || hasNoValue(this.template)) { 554 | return; 555 | } 556 | if (hasNoValue(this.renderer)) { 557 | const dataNodes = this.template.map(node => node.cloneNode(true)); 558 | this.renderer = new DataRenderer(dataNodes, this.getAsset, this.updateDataCallback, () => this.reducer, this.bubbleChildAction, this.updateDataFromChild); 559 | } 560 | const reversedNodes = [...this.renderer.nodes].reverse(); 561 | let anchorNode = document.createElement('template'); 562 | this.append(anchorNode); 563 | for (const node of reversedNodes) { 564 | if (anchorNode.previousSibling !== node) { 565 | this.insertBefore(node, anchorNode); 566 | } 567 | anchorNode = node; 568 | } 569 | const data = this.contextData; 570 | const dataGetter = () => ({ data }); 571 | this.renderer.render(dataGetter); 572 | this.lastChild.remove(); 573 | }; 574 | /** 575 | * initAttribute is the method to initialize ContextElement attribute invoked each time connectedCallback is called. 576 | */ 577 | this.initAttribute = () => { 578 | }; 579 | /** 580 | * Dispatch child action event. 581 | * @param childAction 582 | */ 583 | this.dispatchDetailEvent = (childAction) => { 584 | const event = new CustomEvent(CHILD_ACTION_EVENT, { detail: childAction, cancelable: true, bubbles: true }); 585 | this.dispatchEvent(event); 586 | }; 587 | /** 588 | * Populate the ContextElement template by storing the node child-nodes into template property. 589 | * Once the child nodes is stored in template property, ContextElement will clear its content by calling this.innerHTML = '' 590 | */ 591 | this.populateTemplate = () => { 592 | this.template = Array.from(this.childNodes).filter(noEmptyTextNode()); 593 | this.innerHTML = ''; // we cleanup the innerHTML 594 | }; 595 | /** 596 | * Get the super context element, this function will lookup to the parentNode which is instanceof ContextElement, 597 | * If the parent node is instance of contextElement then this node will return it. 598 | * 599 | * @param parentNode 600 | */ 601 | this.getSuperContextElement = (parentNode) => { 602 | if (parentNode instanceof ContextElement) { 603 | return parentNode; 604 | } 605 | else if (hasValue(parentNode.parentNode)) { 606 | return this.getSuperContextElement(parentNode.parentNode); 607 | } 608 | return null; 609 | }; 610 | this.template = null; 611 | this.renderer = null; 612 | this.reducer = null; 613 | this.contextData = {}; 614 | this.assets = {}; 615 | } 616 | /** 617 | * Get the value of data in this ContextElement 618 | */ 619 | get data() { 620 | return this.contextData; 621 | } 622 | /** 623 | * Set the value of ContextElement data 624 | * @param value 625 | */ 626 | set data(value) { 627 | this.setData(() => value); 628 | } 629 | // noinspection JSUnusedGlobalSymbols 630 | /** 631 | * connectedCallback is invoked each time the custom element is appended into a document-connected element. 632 | * When connectedCallback invoked, it will initialize the active attribute, populate the template, and call 633 | * onMountedCallback. Populating the template will be invoke one time only, the next call of connectedCallback will not 634 | * repopulate the template again. 635 | */ 636 | connectedCallback() { 637 | this.superContextElement = this.getSuperContextElement(this.parentNode); 638 | this.initAttribute(); 639 | if (hasNoValue(this.template)) { 640 | this.classList.add(HIDE_CLASS); 641 | const requestAnimationFrameCallback = () => { 642 | this.populateTemplate(); 643 | this.classList.remove(HIDE_CLASS); 644 | this.render(); 645 | if (hasValue(this.onMountedCallback)) { 646 | this.onMountedCallback(); 647 | this.onMountedCallback = null; 648 | } 649 | }; 650 | //requestAnimationFrame(requestAnimationFrameCallback); 651 | setTimeout(requestAnimationFrameCallback, 0); 652 | } 653 | } 654 | // noinspection JSUnusedGlobalSymbols 655 | /** 656 | * Invoked each time the custom element is disconnected from the document's DOM. 657 | */ 658 | disconnectedCallback() { 659 | this.superContextElement = null; 660 | } 661 | } 662 | 663 | /** 664 | * Error message to show when data.key is missing in context-array 665 | */ 666 | const arrayContextElementMissingDataKey = () => `'' requires 'data.key' attribute. data-key value should refer to the unique attribute of the data.`; 667 | 668 | /** 669 | * ArrayContextElement is ContextElement which can render array instead of javascript object. 670 | * The following is an example of how we display the context-array page. 671 | * 672 | *
673 |      *     
674 |      *         
675 |      *             
676 | *
677 | *
678 | *
679 | * 687 | *
688 | *
689 | * 690 | */ 691 | class ArrayContextElement extends ContextElement { 692 | /** 693 | * Set the default dataKeyPicker using callback that return value of object dataKeyField. 694 | */ 695 | constructor() { 696 | super(); 697 | /** 698 | * DataKeyPicker is a callback function to get the string key value of a data. 699 | * 700 | * @param dataKeyPicker 701 | */ 702 | this.setDataKeyPicker = (dataKeyPicker) => { 703 | this.dataKeyPicker = dataKeyPicker; 704 | }; 705 | /** 706 | * initAttribute store the data.key attribute value to dataKeyField property. 707 | */ 708 | this.initAttribute = () => { 709 | this.dataKeyField = this.getAttribute(DATA_KEY_ATTRIBUTE); 710 | }; 711 | /** 712 | * render method is invoked by the component when it received a new array-update. 713 | * 714 | * It will iterate the array and get the key value of the data. 715 | * It will create a DataRenderer if there is no dataRenderer exist. 716 | * The newly created DataRenderer then stored in the ContextElement renderers Map object along with the key. 717 | * 718 | * Each time ContexElement.render method is invoked, a new callback to get the latest data (dataGetter) is created and passed to 719 | * DataRenderer.render method. 720 | * 721 | */ 722 | this.render = () => { 723 | const contextData = this.contextData; 724 | const template = this.template; 725 | const renderers = this.renderers; 726 | if (hasNoValue(contextData) || hasNoValue(template)) { 727 | return; 728 | } 729 | this.removeExpiredData(); 730 | let anchorNode = document.createElement('template'); 731 | this.append(anchorNode); 732 | const dpLength = contextData.length - 1; 733 | [...contextData].reverse().forEach((data, index) => { 734 | const dataKey = this.dataKeyPicker(data); 735 | if (!renderers.has(dataKey)) { 736 | const dataNode = template.map(node => node.cloneNode(true)); 737 | const itemRenderer = new DataRenderer(dataNode, this.getAsset, this.updateDataCallback, () => this.reducer, this.bubbleChildAction, this.updateDataFromChild); 738 | renderers.set(dataKey, itemRenderer); 739 | } 740 | const itemRenderer = renderers.get(dataKey); 741 | const reversedNodes = [...itemRenderer.nodes].reverse(); 742 | for (const node of reversedNodes) { 743 | if (anchorNode.previousSibling !== node) { 744 | this.insertBefore(node, anchorNode); 745 | } 746 | anchorNode = node; 747 | } 748 | const dataGetter = () => ({ data, key: dataKey, index: (dpLength - index) }); 749 | itemRenderer.render(dataGetter); 750 | }); 751 | this.lastChild.remove(); 752 | }; 753 | /** 754 | * Function to remove keys that is no longer exist in the ContextElement.renderers. 755 | * When ContextElement received new data (dataSource),it will check the obsolete keys in the ContextElement.renderers. 756 | * The obsolate keys along with the DataRenderer attach to it, removed from the ContextElement.renderers, and the template 757 | * node removed from the document.body. 758 | */ 759 | this.removeExpiredData = () => { 760 | const renderers = this.renderers; 761 | const contextData = this.contextData; 762 | const dataSourceKeys = contextData.map(data => this.dataKeyPicker(data)); 763 | const prevKeys = Array.from(renderers.keys()); 764 | const discardedKeys = prevKeys.filter(key => dataSourceKeys.indexOf(key) < 0); 765 | discardedKeys.forEach(discardedKey => { 766 | const discardNode = (node) => node.remove(); 767 | renderers.get(discardedKey).nodes.forEach(discardNode); 768 | renderers.delete(discardedKey); 769 | }); 770 | }; 771 | const defaultDataKeyPicker = (data) => { 772 | if (hasNoValue(this.dataKeyField)) { 773 | throw new Error(arrayContextElementMissingDataKey()); 774 | } 775 | return data[this.dataKeyField]; 776 | }; 777 | this.renderers = new Map(); 778 | this.dataKeyPicker = defaultDataKeyPicker; 779 | this.contextData = []; 780 | } 781 | // noinspection JSUnusedGlobalSymbols 782 | /** 783 | * Observed attributes in context element 784 | */ 785 | static get observedAttributes() { 786 | return [DATA_KEY_ATTRIBUTE]; 787 | } 788 | // noinspection JSUnusedGlobalSymbols 789 | /** 790 | * update the dataKeyField if there's a new change in the attribute. 791 | * 792 | * @param name of the attribute 793 | * @param oldValue 794 | * @param newValue 795 | */ 796 | attributeChangedCallback(name, oldValue, newValue) { 797 | if (name === DATA_KEY_ATTRIBUTE) { 798 | this.dataKeyField = newValue; 799 | } 800 | } 801 | } 802 | 803 | customElements.define(ARRAY_CONTEXT_ELEMENT_TAG_NAME, ArrayContextElement); 804 | customElements.define(CONTEXT_ELEMENT_TAG_NAME, ContextElement); 805 | 806 | }()); 807 | -------------------------------------------------------------------------------- /index.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";const t=t=>t+"Changed",e=t=>null!=t&&""!==t,a=t=>!e(t),i=(t,e)=>e.reduce((e,a)=>e||t.indexOf(a)>=0,!1),s=document.createElement("style");function n(){return t=>t.nodeType!==Node.TEXT_NODE||/\S/.test(t.textContent)}s.innerHTML=".data-element-hidden {display: none !important;}",document.head.appendChild(s);const r=["data","reducer"];class o{constructor(t,e,a,i,s,n,r){this.attributeProperty=null,this.eventAction=null,this.render=()=>{const t=this.activeNode,e=this.attributeProperty,a=this.dataGetter().data,i=this.assetGetter;p(t,e,a,i)},this.activeNode=t,this.dataGetter=a,this.assetGetter=e,this.updateData=i,this.bubbleChildAction=r,this.reducerGetter=s,this.activeAttributeValue=c(t,n),this.eventAction=h(this.activeAttributeValue),this.attributeProperty=d(this.activeAttributeValue,["watch","asset"]),l(t,this.eventAction,a,i,s,r)}}const h=t=>{const e=new Map;return t.forEach((t,a)=>{if(a.endsWith("action")){const i=a.split(".");let s="";s=1===i.length?"click":i[0],e.set(s,t)}}),e},d=(t,e)=>{const a=new Map;return t.forEach((t,i)=>{if(e.filter(t=>i.endsWith(t)).length>0){const e=i.split(".");let s="",n="";1===e.length?(s="content",n=e[0]):(s=e[0],n=e[1]),a.has(s)||a.set(s,new Map),a.get(s).set(n,t)}}),a},c=(t,e)=>{const a=new Map;return t.getAttributeNames().filter(t=>i(t,e)).forEach(e=>{a.set(e,t.getAttribute(e)),t.removeAttribute(e)}),a},l=(t,e,i,s,n,r)=>{e.forEach((e,o)=>{o=o.startsWith("on")?o.substring("on".length,o.length):o,t.addEventListener(o,t=>{t.preventDefault(),t.stopImmediatePropagation();const o=i(),h=n(),d=e;let c=o.data;const l={type:d,event:t};if("key"in o){const t=o;c=t.data,l.data=c,l.key=t.key,l.index=t.index}a(h)?r(l):s(t=>h(t,l))})})};function u(e,a,i,s,n){var o;if(o=e,r.indexOf(o)<0&&a.getAttribute(e)!==i&&a.setAttribute(e,i),e in a){a[e]=i,"data"===e&&(a.dataPath=n);a[t(e)]=t=>f(s,n,t)}"content"===e&&(a.innerHTML=i)}const p=(t,i,s,n)=>{const r=i;a(r)||r.forEach((a,i)=>{const r=a.get("watch"),o=a.get("asset");let h=null;e(r)?h=m(s,r):e(o)&&(h=n(o)),u(i,t,h,s,r)})},m=(t,e)=>{if(a(t))return t;try{return new Function("data",`return data.${e};`).call(null,t)}catch(e){console.warn(t,e.message)}return null},f=(t,e,i)=>{if(!a(t))try{return new Function("data","value",`data.${e} = value;`).call(null,t,i)}catch(t){console.warn(t.message)}};class b{constructor(t,e,a,i,s,n){this.render=t=>{this.dataGetter=t,this.attributeEvaluators.forEach(t=>t.render())},this.nodes=t,this.addChildActionEventListener(n);const r=["watch","action","asset"],h=Array.from(y(r,t)),d=()=>this.dataGetter();this.attributeEvaluators=h.map(t=>new o(t,e,d,a,i,r,s))}addChildActionEventListener(t){this.nodes.forEach(e=>{e.addEventListener("childAction",e=>{if(e.defaultPrevented)return;e.stopImmediatePropagation(),e.stopPropagation(),e.preventDefault();const a=e.detail,i=this.dataGetter(),s={index:i.index,event:a.event,type:a.type,data:i.data,key:i.key};t(a,s)})})}}const y=(t,e)=>e.filter(n()).reduce((e,a)=>{if(!(a instanceof HTMLElement))return e;const s=a,n=s.getAttributeNames();for(const a of n)i(a,t)&&e.add(s);if(!i(s.tagName,["context-array".toUpperCase(),"context-element".toUpperCase()])){const a=y(t,Array.from(s.childNodes));Array.from(a).forEach(t=>e.add(t))}return e},new Set);class v extends HTMLElement{constructor(){super(),this.setData=t=>{this.contextData=t(this.contextData),this.render()},this.onMounted=t=>{this.onMountedCallback=t},this.getAsset=t=>{const a=this.assets;if(e(a)&&t in a)return a[t];const i=this.superContextElement;return e(i)?i.getAsset(t):null},this.actionToPath=t=>{const a={path:this.dataPath};return e(t.key)&&(a.key=t.key,a.index=t.index,a.data=t.data),a},this.updateDataCallback=e=>{this.setData(e);const a=t("data");a in this&&this[a].call(this,this.contextData)},this.bubbleChildAction=t=>{const e={event:t.event,type:t.type,childActions:[this.actionToPath(t)]};this.dispatchDetailEvent(e)},this.updateDataFromChild=(t,e)=>{const i=this.reducer;a(i)?(t.childActions=[this.actionToPath(e),...t.childActions],this.dispatchDetailEvent(t)):this.updateDataCallback(e=>i(e,t))},this.render=()=>{if(a(this.contextData)||a(this.template))return;if(a(this.renderer)){const t=this.template.map(t=>t.cloneNode(!0));this.renderer=new b(t,this.getAsset,this.updateDataCallback,()=>this.reducer,this.bubbleChildAction,this.updateDataFromChild)}const t=[...this.renderer.nodes].reverse();let e=document.createElement("template");this.append(e);for(const a of t)e.previousSibling!==a&&this.insertBefore(a,e),e=a;const i=this.contextData;this.renderer.render(()=>({data:i})),this.lastChild.remove()},this.initAttribute=()=>{},this.dispatchDetailEvent=t=>{const e=new CustomEvent("childAction",{detail:t,cancelable:!0,bubbles:!0});this.dispatchEvent(e)},this.populateTemplate=()=>{this.template=Array.from(this.childNodes).filter(n()),this.innerHTML=""},this.getSuperContextElement=t=>t instanceof v?t:e(t.parentNode)?this.getSuperContextElement(t.parentNode):null,this.template=null,this.renderer=null,this.reducer=null,this.contextData={},this.assets={}}get data(){return this.contextData}set data(t){this.setData(()=>t)}connectedCallback(){if(this.superContextElement=this.getSuperContextElement(this.parentNode),this.initAttribute(),a(this.template)){this.classList.add("data-element-hidden");setTimeout(()=>{this.populateTemplate(),this.classList.remove("data-element-hidden"),this.render(),e(this.onMountedCallback)&&(this.onMountedCallback(),this.onMountedCallback=null)},0)}}disconnectedCallback(){this.superContextElement=null}}customElements.define("context-array",class extends v{constructor(){super(),this.setDataKeyPicker=t=>{this.dataKeyPicker=t},this.initAttribute=()=>{this.dataKeyField=this.getAttribute("data.key")},this.render=()=>{const t=this.contextData,e=this.template,i=this.renderers;if(a(t)||a(e))return;this.removeExpiredData();let s=document.createElement("template");this.append(s);const n=t.length-1;[...t].reverse().forEach((t,a)=>{const r=this.dataKeyPicker(t);if(!i.has(r)){const t=e.map(t=>t.cloneNode(!0)),a=new b(t,this.getAsset,this.updateDataCallback,()=>this.reducer,this.bubbleChildAction,this.updateDataFromChild);i.set(r,a)}const o=i.get(r),h=[...o.nodes].reverse();for(const t of h)s.previousSibling!==t&&this.insertBefore(t,s),s=t;o.render(()=>({data:t,key:r,index:n-a}))}),this.lastChild.remove()},this.removeExpiredData=()=>{const t=this.renderers,e=this.contextData.map(t=>this.dataKeyPicker(t));Array.from(t.keys()).filter(t=>e.indexOf(t)<0).forEach(e=>{t.get(e).nodes.forEach(t=>t.remove()),t.delete(e)})};this.renderers=new Map,this.dataKeyPicker=t=>{if(a(this.dataKeyField))throw new Error("'' requires 'data.key' attribute. data-key value should refer to the unique attribute of the data.");return t[this.dataKeyField]},this.contextData=[]}static get observedAttributes(){return["data.key"]}attributeChangedCallback(t,e,a){"data.key"===t&&(this.dataKeyField=a)}}),customElements.define("context-element",v)}(); 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src'], 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest', 5 | }, 6 | collectCoverage:true, 7 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "context-element", 3 | "version": "0.0.0-development", 4 | "description": "An HTMLElement that makes it easy to render data or array in html page", 5 | "main": "index.js", 6 | "keywords": [ 7 | "webcomponent", 8 | "html" 9 | ], 10 | "scripts": { 11 | "test": "jest --coverage", 12 | "build": "rollup --config rollup.config.js", 13 | "todo": "parcel ./example/todo/todo.html", 14 | "commit": "git-cz", 15 | "storybook": "start-storybook", 16 | "build-storybook": "build-storybook -c .storybook -o docs" 17 | }, 18 | "author": "Arif Rachim", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@babel/core": "^7.10.3", 22 | "@babel/parser": "^7.10.3", 23 | "@babel/types": "^7.10.3", 24 | "@storybook/addon-knobs": "^5.3.19", 25 | "@storybook/addon-storysource": "^5.3.19", 26 | "@storybook/html": "^5.3.19", 27 | "@storybook/preset-typescript": "^3.0.0", 28 | "@types/faker": "^4.1.12", 29 | "@types/jest": "^26.0.0", 30 | "@types/node": "^14.0.13", 31 | "babel-loader": "^8.1.0", 32 | "codecov": "^3.7.0", 33 | "commitizen": "^4.1.2", 34 | "cz-conventional-changelog": "^3.2.0", 35 | "faker": "^4.1.0", 36 | "fork-ts-checker-webpack-plugin": "^5.0.5", 37 | "jest": "^26.0.1", 38 | "parcel-bundler": "^1.12.4", 39 | "rollup": "^2.17.1", 40 | "rollup-plugin-terser": "^6.1.0", 41 | "rollup-plugin-typescript2": "^0.27.1", 42 | "rollup-plugin-uglify": "^6.0.4", 43 | "semantic-release": "^17.1.0", 44 | "ts-jest": "^26.1.0", 45 | "ts-loader": "^7.0.5", 46 | "typescript": "^3.9.5" 47 | }, 48 | "dependencies": {}, 49 | "repository": { 50 | "type": "git", 51 | "url": "https://github.com/marsa-emreef/context-element.git" 52 | }, 53 | "czConfig": { 54 | "path": "node_modules/cz-conventional-changelog" 55 | }, 56 | "release": { 57 | "branches": [ 58 | "master" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import typescript from 'rollup-plugin-typescript2'; 3 | import {terser} from "rollup-plugin-terser"; 4 | 5 | const pkg = require('./package'); 6 | 7 | const minOutputFile = (name) => { 8 | const splitDots = name.split('.'); 9 | return [...splitDots.slice(0,splitDots.length-1),'min',splitDots[splitDots.length-1]].join('.'); 10 | }; 11 | 12 | export default { 13 | input: 'src/index.ts', 14 | output: [ 15 | { 16 | file: pkg.main, 17 | format: 'iife', 18 | }, 19 | { 20 | file: minOutputFile(pkg.main), 21 | format: 'iife', 22 | plugins: [terser()] 23 | } 24 | ], 25 | plugins: [typescript()] 26 | }; 27 | -------------------------------------------------------------------------------- /src/array-context-element.spec.ts: -------------------------------------------------------------------------------- 1 | import {ArrayContextElement} from './array-context-element'; 2 | import './index'; 3 | import * as faker from 'faker'; 4 | import uuid from "./libs/uuid"; 5 | import {ChildAction, DATA_KEY_ATTRIBUTE} from "./types"; 6 | 7 | const createArrayContextElement = (innerHTML?: string) => { 8 | document.body.innerHTML = ''; 9 | const randomId = uuid(); 10 | const element = document.createElement('context-array'); 11 | element.innerHTML = innerHTML; 12 | element.setAttribute('id', randomId); 13 | document.body.append(element); 14 | return document.body.querySelector(`#${randomId}`) as ArrayContextElement; 15 | }; 16 | 17 | const generateRandomUser = (length: number) => { 18 | return Array.from({length}).map(() => ({ 19 | name: `${faker.name.firstName()} ${faker.name.lastName()}`, 20 | city: faker.address.city(), 21 | picture: faker.image.avatar(), 22 | userId: Math.random() * 1000000 23 | })); 24 | }; 25 | 26 | test('It should create an element of ArrayContextElement', () => { 27 | const group = createArrayContextElement(); 28 | expect(true).toBe(group instanceof ArrayContextElement); 29 | }); 30 | 31 | 32 | test('It should throw error when setting dataSource without keySelector', (done) => { 33 | const arrayContextElement = createArrayContextElement(`
`); 34 | const users = generateRandomUser(10); 35 | arrayContextElement.onMounted(() => { 36 | expect(() => { 37 | arrayContextElement.setData(() => users); 38 | }).toThrow(); 39 | done(); 40 | }); 41 | 42 | }); 43 | 44 | // rendering child noe 45 | test('It should render the childNodes and validate the length based on dataSource', (done) => { 46 | const arrayContextElement = createArrayContextElement(`
`); 47 | const users = generateRandomUser(10); 48 | arrayContextElement.setDataKeyPicker((data) => data.userId); 49 | arrayContextElement.setData(() => users); 50 | arrayContextElement.onMounted(() => { 51 | expect(arrayContextElement.childNodes.length).toBe(10); 52 | done(); 53 | }); 54 | }); 55 | 56 | test('It should perform remove', (done) => { 57 | const arrayContextElement = createArrayContextElement(`
`); 58 | const users = generateRandomUser(10); 59 | arrayContextElement.setDataKeyPicker((data) => data.userId); 60 | arrayContextElement.setData(() => users); 61 | arrayContextElement.onMounted(() => { 62 | expect(arrayContextElement.childNodes.length).toBe(10); 63 | arrayContextElement.setData(() => []); 64 | expect(arrayContextElement.childNodes.length).toBe(0); 65 | arrayContextElement.setData(() => generateRandomUser(4)); 66 | expect(arrayContextElement.childNodes.length).toBe(4); 67 | done(); 68 | }); 69 | }); 70 | 71 | 72 | test('It should bind event against node', (done) => { 73 | const arrayContextElement = createArrayContextElement(''); 74 | arrayContextElement.setAttribute(DATA_KEY_ATTRIBUTE, 'userId'); 75 | arrayContextElement.onMounted(() => { 76 | arrayContextElement.data = generateRandomUser(5); 77 | expect(arrayContextElement.childNodes.length).toEqual(5); 78 | expect(arrayContextElement.firstChild instanceof HTMLInputElement).toBe(true); 79 | expect(arrayContextElement.lastChild instanceof HTMLInputElement).toBe(true); 80 | done(); 81 | }); 82 | }); 83 | 84 | 85 | test('It should assign the value from assets', (done) => { 86 | const contextElement = createArrayContextElement(` 87 |
88 |
89 | 90 |
91 | 92 |
93 |
94 | `); 95 | contextElement.data = [{id: 1}, {id: 2}, {id: 3}, {id: 4}]; 96 | contextElement.setAttribute('data.key', 'id'); 97 | contextElement.assets = { 98 | kambing: 'kambing', 99 | helloWorld: (data: any, action: any) => { 100 | const {type} = action; 101 | if (type === 'SET_CONTENT') { 102 | { 103 | return {...data, content: 'Hello World'} 104 | } 105 | } 106 | return {...data} 107 | } 108 | }; 109 | setTimeout(() => { 110 | const buttons = contextElement.querySelectorAll('.button-gila'); 111 | const contents = contextElement.querySelectorAll('.contend'); 112 | const kambings = contextElement.querySelectorAll('.kambing'); 113 | expect(buttons.length).toBe(contextElement.data.length); 114 | kambings.forEach(kambing => { 115 | expect(kambing.innerHTML).toBe("kambing"); 116 | }); 117 | contents.forEach(content => { 118 | expect(content.innerHTML).toBe("undefined"); 119 | }); 120 | buttons.forEach((button: any) => { 121 | button.click() 122 | }); 123 | contents.forEach(content => { 124 | expect(content.innerHTML).toBe("Hello World"); 125 | }); 126 | done(); 127 | }, 100); 128 | 129 | }); 130 | 131 | test(`Context Array should bubble the action child`,(done) => { 132 | const ce = createArrayContextElement(`
133 | 134 | 135 | 136 |
137 |
138 |
139 |
`); 140 | ce.setDataKeyPicker((data) => data.id); 141 | ce.data = [{ 142 | id : 'dictionary', 143 | persons : [ 144 | { 145 | id: 'person', 146 | addresses : [{ 147 | id : 'dubai', 148 | city : 'DUBAI' 149 | }] 150 | } 151 | ] 152 | }]; 153 | 154 | ce.reducer = (data,action) => { 155 | const [firstPath,secondPath] = (action as ChildAction).childActions; 156 | secondPath.data.city = 'TOKYO'; 157 | return [...data]; 158 | }; 159 | 160 | setTimeout(() => { 161 | const button = document.getElementById('buttonContext'); 162 | const cityDiv = document.getElementById('city'); 163 | expect(cityDiv.innerHTML).toBe('DUBAI'); 164 | button.click(); 165 | setTimeout(() => { 166 | expect(cityDiv.innerHTML).toBe('TOKYO'); 167 | done(); 168 | },100); 169 | },100); 170 | 171 | }); 172 | -------------------------------------------------------------------------------- /src/array-context-element.ts: -------------------------------------------------------------------------------- 1 | import {DATA_KEY_ATTRIBUTE, DataGetter, hasNoValue, Renderer, ToString} from "./types"; 2 | import {ContextElement} from "./context-element"; 3 | import {arrayContextElementMissingDataKey} from "./libs/error-message"; 4 | import DataRenderer from "./libs/data-renderer"; 5 | 6 | /** 7 | * ArrayContextElement is ContextElement which can render array instead of javascript object. 8 | * The following is an example of how we display the context-array page. 9 | * 10 | *
 11 |  *     
 12 |  *         
 13 |  *             
14 | *
15 | *
16 | *
17 | * 25 | *
26 | *
27 | * 28 | */ 29 | export class ArrayContextElement extends ContextElement { 30 | 31 | private dataKeyPicker: ToString; 32 | private dataKeyField: string; 33 | private readonly renderers: Map; 34 | 35 | /** 36 | * Set the default dataKeyPicker using callback that return value of object dataKeyField. 37 | */ 38 | constructor() { 39 | super(); 40 | const defaultDataKeyPicker = (data: Context) => { 41 | if (hasNoValue(this.dataKeyField)) { 42 | throw new Error(arrayContextElementMissingDataKey()); 43 | } 44 | return (data as any)[this.dataKeyField]; 45 | }; 46 | this.renderers = new Map(); 47 | this.dataKeyPicker = defaultDataKeyPicker; 48 | this.contextData = []; 49 | } 50 | 51 | // noinspection JSUnusedGlobalSymbols 52 | /** 53 | * Observed attributes in context element 54 | */ 55 | static get observedAttributes() { 56 | return [DATA_KEY_ATTRIBUTE]; 57 | } 58 | 59 | /** 60 | * DataKeyPicker is a callback function to get the string key value of a data. 61 | * 62 | * @param dataKeyPicker 63 | */ 64 | public setDataKeyPicker = (dataKeyPicker: ToString) => { 65 | this.dataKeyPicker = dataKeyPicker; 66 | }; 67 | 68 | // noinspection JSUnusedGlobalSymbols 69 | /** 70 | * update the dataKeyField if there's a new change in the attribute. 71 | * 72 | * @param name of the attribute 73 | * @param oldValue 74 | * @param newValue 75 | */ 76 | attributeChangedCallback(name: string, oldValue: string, newValue: string) { 77 | if (name === DATA_KEY_ATTRIBUTE) { 78 | this.dataKeyField = newValue; 79 | } 80 | } 81 | 82 | /** 83 | * initAttribute store the data.key attribute value to dataKeyField property. 84 | */ 85 | protected initAttribute = () => { 86 | this.dataKeyField = this.getAttribute(DATA_KEY_ATTRIBUTE); 87 | }; 88 | 89 | /** 90 | * render method is invoked by the component when it received a new array-update. 91 | * 92 | * It will iterate the array and get the key value of the data. 93 | * It will create a DataRenderer if there is no dataRenderer exist. 94 | * The newly created DataRenderer then stored in the ContextElement renderers Map object along with the key. 95 | * 96 | * Each time ContexElement.render method is invoked, a new callback to get the latest data (dataGetter) is created and passed to 97 | * DataRenderer.render method. 98 | * 99 | */ 100 | protected render = () => { 101 | const contextData: Context[] = this.contextData; 102 | const template: ChildNode[] = this.template; 103 | const renderers: Map = this.renderers; 104 | 105 | if (hasNoValue(contextData) || hasNoValue(template)) { 106 | return; 107 | } 108 | 109 | this.removeExpiredData(); 110 | let anchorNode: Node = document.createElement('template'); 111 | this.append(anchorNode); 112 | const dpLength = contextData.length - 1; 113 | [...contextData].reverse().forEach((data: Context, index: number) => { 114 | const dataKey = this.dataKeyPicker(data); 115 | if (!renderers.has(dataKey)) { 116 | const dataNode: ChildNode[] = template.map(node => node.cloneNode(true)) as ChildNode[]; 117 | const itemRenderer = new DataRenderer(dataNode, this.getAsset, this.updateDataCallback, () => this.reducer, this.bubbleChildAction, this.updateDataFromChild); 118 | renderers.set(dataKey, itemRenderer); 119 | } 120 | const itemRenderer = renderers.get(dataKey); 121 | const reversedNodes = [...itemRenderer.nodes].reverse(); 122 | for (const node of reversedNodes) { 123 | if (anchorNode.previousSibling !== node) { 124 | this.insertBefore(node, anchorNode); 125 | } 126 | anchorNode = node; 127 | } 128 | const dataGetter: DataGetter = () => ({data, key: dataKey, index: (dpLength - index)}); 129 | itemRenderer.render(dataGetter); 130 | }); 131 | this.lastChild.remove(); 132 | }; 133 | 134 | /** 135 | * Function to remove keys that is no longer exist in the ContextElement.renderers. 136 | * When ContextElement received new data (dataSource),it will check the obsolete keys in the ContextElement.renderers. 137 | * The obsolate keys along with the DataRenderer attach to it, removed from the ContextElement.renderers, and the template 138 | * node removed from the document.body. 139 | */ 140 | private removeExpiredData = () => { 141 | const renderers: Map = this.renderers; 142 | const contextData: Context[] = this.contextData; 143 | const dataSourceKeys = contextData.map(data => this.dataKeyPicker(data)); 144 | const prevKeys = Array.from(renderers.keys()); 145 | const discardedKeys = prevKeys.filter(key => dataSourceKeys.indexOf(key) < 0); 146 | discardedKeys.forEach(discardedKey => { 147 | const discardNode = (node: ChildNode) => node.remove(); 148 | renderers.get(discardedKey).nodes.forEach(discardNode); 149 | renderers.delete(discardedKey); 150 | }); 151 | }; 152 | } 153 | 154 | -------------------------------------------------------------------------------- /src/context-element.spec.ts: -------------------------------------------------------------------------------- 1 | import {ContextElement} from './context-element'; 2 | import './index'; 3 | import * as faker from 'faker'; 4 | import uuid from "./libs/uuid"; 5 | import {Action} from "./types"; 6 | 7 | /** 8 | * 9 | * @param innerHTML 10 | */ 11 | const createContextElement = (innerHTML?: string) => { 12 | document.body.innerHTML = ''; 13 | const randomId = uuid(); 14 | const element = document.createElement('context-element'); 15 | element.innerHTML = innerHTML; 16 | element.setAttribute('id', randomId); 17 | document.body.append(element); 18 | return document.body.querySelector(`#${randomId}`) as ContextElement; 19 | }; 20 | 21 | const generateRandomUser = (length: number) => { 22 | return Array.from({length}).map(() => ({ 23 | name: `${faker.name.firstName()} ${faker.name.lastName()}`, 24 | city: faker.address.city(), 25 | picture: faker.image.avatar(), 26 | userId: Math.random() * 1000000 27 | })); 28 | }; 29 | 30 | test('It should create an element of ContextElement', () => { 31 | const group = createContextElement(); 32 | expect(true).toBe(group instanceof ContextElement); 33 | }); 34 | 35 | test('It should render the childNodes and validate based on data', (done) => { 36 | const contextElement = createContextElement(`
`); 37 | const users = generateRandomUser(1); 38 | contextElement.data = users[0]; 39 | contextElement.onMounted(() => { 40 | expect((contextElement.firstChild as HTMLDivElement).innerHTML).toBe(contextElement.data.name); 41 | done(); 42 | }); 43 | }); 44 | 45 | test('It should update the childNodes and validate based on data', (done) => { 46 | const contextElement = createContextElement(`
`); 47 | const users = generateRandomUser(5); 48 | contextElement.data = users[2]; 49 | contextElement.onMounted(() => { 50 | expect((contextElement.firstChild as HTMLDivElement).innerHTML).toBe(contextElement.data.name); 51 | contextElement.data = users[1]; 52 | expect((contextElement.firstChild as HTMLDivElement).innerHTML).toBe(users[1].name); 53 | contextElement.data = users[3]; 54 | expect((contextElement.firstChild as HTMLDivElement).innerHTML).toBe(users[3].name); 55 | done(); 56 | }); 57 | }); 58 | 59 | test('It should update the childNodes and validate based data state', (done) => { 60 | const contextElement = createContextElement(` 61 |
64 | `); 65 | const users = generateRandomUser(5); 66 | contextElement.data = users[2]; 67 | contextElement.onMounted(() => { 68 | expect((contextElement.firstChild as HTMLDivElement).innerHTML).toBe(contextElement.data.name); 69 | let data = users[1]; 70 | contextElement.data = data; 71 | expect((contextElement.firstChild as HTMLDivElement).innerHTML).toBe(users[1].name); 72 | contextElement.data = users[3]; 73 | expect((contextElement.firstChild as HTMLDivElement).innerHTML).toBe(users[3].name); 74 | done(); 75 | }); 76 | }); 77 | 78 | test('It should perform update only against the node leaf', (done) => { 79 | const contextElement = createContextElement(`
80 | 81 |
82 | 83 |
`); 84 | contextElement.data = { 85 | name: 'Example of how to change the node', 86 | address: { 87 | city: 'You can change this value' 88 | }, 89 | addressReducer: (address: any, action: Action) => { 90 | const type: string = action.type; 91 | const event: any = action.event; 92 | if (type === 'SET_CITY') { 93 | { 94 | const city = event.target.value; 95 | return {...address, city} 96 | } 97 | } 98 | return {...address} 99 | } 100 | }; 101 | contextElement.onMounted(() => { 102 | const subElement = document.getElementById('subElement') as ContextElement; 103 | subElement.onMounted(() => { 104 | const input: HTMLInputElement = document.getElementById('input') as HTMLInputElement; 105 | input.value = 'Jakarta'; 106 | input.dispatchEvent(new InputEvent('input', {bubbles: true, cancelable: true})); 107 | const cityDiv = document.getElementById('cityDiv'); 108 | expect(cityDiv.innerHTML).toBe('Jakarta'); 109 | done(); 110 | }); 111 | }); 112 | 113 | }); 114 | 115 | 116 | test('it should get the assets from the context element', (done) => { 117 | const contextElement = createContextElement(` 118 |
119 | 120 |
121 | 122 | 123 | 124 |
125 |
126 | `); 127 | const myReducer = (state: any) => { 128 | return {...state} 129 | }; 130 | contextElement.assets = { 131 | myReducer, 132 | name: 'sedap' 133 | }; 134 | contextElement.onMounted(() => { 135 | const childContextElement = document.getElementById('child') as ContextElement; 136 | 137 | expect(childContextElement.getAsset('myReducer')).toBe(myReducer); 138 | expect(childContextElement.getAsset('name')).toBe('sedap'); 139 | childContextElement.assets = { 140 | name: 'kuncup' 141 | }; 142 | expect(childContextElement.getAsset('name')).toBe('kuncup'); 143 | done(); 144 | }); 145 | 146 | }); 147 | 148 | test('It should assign the value from assets', (done) => { 149 | const contextElement = createContextElement(` 150 |
151 |
152 | 153 |
154 | 155 |
156 |
157 | `); 158 | contextElement.assets = { 159 | kambing: 'kambing', 160 | helloWorld: (data: any, action: any) => { 161 | const {type} = action; 162 | if (type === 'SET_CONTENT') { 163 | { 164 | return {...data, content: 'Hello World'} 165 | } 166 | } 167 | return {...data} 168 | } 169 | }; 170 | setTimeout(() => { 171 | const myButton = document.getElementById('buttonGila'); 172 | const contentDiv = document.getElementById('contentDiv'); 173 | const kambingDiv = document.getElementById('kambingId'); 174 | expect(kambingDiv.innerHTML).toBe("kambing"); 175 | expect(contentDiv.innerHTML).toBe("undefined"); 176 | myButton.click(); 177 | expect(contentDiv.innerHTML).toBe('Hello World'); 178 | done(); 179 | }, 100); 180 | 181 | }); 182 | 183 | test(`It should bubble the action child`,(done) => { 184 | const ce = createContextElement(`
185 | 186 | 187 | 188 |
189 |
190 |
191 |
`); 192 | ce.data = { 193 | person : { 194 | address : { 195 | city : 'DUBAI' 196 | } 197 | } 198 | }; 199 | 200 | ce.reducer = (data,action) => { 201 | return { 202 | person : { 203 | address : { 204 | city : 'TOKYO' 205 | } 206 | } 207 | }; 208 | }; 209 | 210 | setTimeout(() => { 211 | const button = document.getElementById('buttonContext'); 212 | const cityDiv = document.getElementById('city'); 213 | expect(cityDiv.innerHTML).toBe('DUBAI'); 214 | button.click(); 215 | setTimeout(() => { 216 | expect(cityDiv.innerHTML).toBe('TOKYO'); 217 | done(); 218 | },100); 219 | },100); 220 | 221 | }); -------------------------------------------------------------------------------- /src/context-element.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | ActionPath, 4 | ArrayAction, 5 | CHILD_ACTION_EVENT, 6 | ChildAction, 7 | composeChangeEventName, 8 | DataGetter, 9 | DataSetter, 10 | hasNoValue, 11 | hasValue, 12 | HIDE_CLASS, 13 | Reducer 14 | } from "./types"; 15 | import noEmptyTextNode from "./libs/no-empty-text-node"; 16 | import DataRenderer from "./libs/data-renderer"; 17 | 18 | /** 19 | * ContextElement is HTMLElement which can render data in accordance with the template defined in it. 20 | * The following is an example of how we display the template page. 21 | * 22 | *
 23 |  *     
 24 |  *         
 25 |  *             
26 | *
27 | *
28 | *
29 | * 33 | *
34 | *
35 | * 36 | * ContextElement will populate the data into template by looking at the attribute which has watch keyword in it. 37 | * These attribute which has keyword `watch` in it are also known as active-attribute. 38 | * There are 4 kinds of active-attribute, (watch / toggle / action / assets). each attribute works with a different mechanism when ContextElement renders the data. 39 | * 40 | */ 41 | export class ContextElement extends HTMLElement { 42 | public reducer: Reducer; 43 | public assets: any; 44 | public dataPath: string; 45 | protected template: ChildNode[]; 46 | protected renderer: DataRenderer; 47 | protected contextData: Context; 48 | protected onMountedCallback: () => void; 49 | private superContextElement: ContextElement; 50 | 51 | /** 52 | * Constructor sets default value of reducer to return the parameter immediately (param) => param. 53 | */ 54 | constructor() { 55 | super(); 56 | this.template = null; 57 | this.renderer = null; 58 | this.reducer = null; 59 | this.contextData = {} as Context; 60 | this.assets = {}; 61 | } 62 | 63 | /** 64 | * Get the value of data in this ContextElement 65 | */ 66 | get data(): Context { 67 | return this.contextData; 68 | } 69 | 70 | /** 71 | * Set the value of ContextElement data 72 | * @param value 73 | */ 74 | set data(value: Context) { 75 | this.setData(() => value); 76 | } 77 | 78 | /** 79 | * Callback function to set the data, 80 | *
 81 |      *     
 82 |      *         contextElement.setData(data => ({...data,attribute:newValue});
 83 |      *     
 84 |      * 
85 | * 86 | * @param context 87 | */ 88 | public setData = (context: DataSetter) => { 89 | this.contextData = context(this.contextData); 90 | this.render(); 91 | }; 92 | 93 | /** 94 | * onMounted is invoke when the Element is ready and mounted to the window.document. 95 | *
 96 |      *     
 97 |      *         contextElement.onMounted(() => console.log(`ChildNodes Ready `,contextElement.childNodes.length > 0));
 98 |      *     
 99 |      * 
100 | * @param onMountedListener 101 | */ 102 | public onMounted = (onMountedListener: () => void) => { 103 | this.onMountedCallback = onMountedListener; 104 | }; 105 | 106 | // noinspection JSUnusedGlobalSymbols 107 | /** 108 | * connectedCallback is invoked each time the custom element is appended into a document-connected element. 109 | * When connectedCallback invoked, it will initialize the active attribute, populate the template, and call 110 | * onMountedCallback. Populating the template will be invoke one time only, the next call of connectedCallback will not 111 | * repopulate the template again. 112 | */ 113 | connectedCallback() { 114 | this.superContextElement = this.getSuperContextElement(this.parentNode); 115 | this.initAttribute(); 116 | if (hasNoValue(this.template)) { 117 | this.classList.add(HIDE_CLASS); 118 | const requestAnimationFrameCallback = () => { 119 | this.populateTemplate(); 120 | this.classList.remove(HIDE_CLASS); 121 | 122 | this.render(); 123 | if (hasValue(this.onMountedCallback)) { 124 | this.onMountedCallback(); 125 | this.onMountedCallback = null; 126 | } 127 | }; 128 | //requestAnimationFrame(requestAnimationFrameCallback); 129 | setTimeout(requestAnimationFrameCallback, 0); 130 | } 131 | } 132 | 133 | // noinspection JSUnusedGlobalSymbols 134 | /** 135 | * Invoked each time the custom element is disconnected from the document's DOM. 136 | */ 137 | disconnectedCallback() { 138 | this.superContextElement = null; 139 | } 140 | 141 | /** 142 | * Get the assets from the current assets or the parent context element assets. 143 | * @param key 144 | */ 145 | public getAsset = (key: string): any => { 146 | const assets = this.assets; 147 | if (hasValue(assets) && key in assets) { 148 | return assets[key]; 149 | } 150 | const superContextElement = this.superContextElement; 151 | if (hasValue(superContextElement)) { 152 | return superContextElement.getAsset(key); 153 | } 154 | return null; 155 | }; 156 | 157 | /** 158 | * Convert action to ActionPath 159 | * @param arrayAction 160 | */ 161 | actionToPath = (arrayAction: ArrayAction) => { 162 | const actionPath: ActionPath = {path: this.dataPath}; 163 | if (hasValue(arrayAction.key)) { 164 | actionPath.key = arrayAction.key; 165 | actionPath.index = arrayAction.index; 166 | actionPath.data = arrayAction.data; 167 | } 168 | return actionPath; 169 | }; 170 | 171 | /** 172 | * updateDataCallback is a callback function that will set the data and call `dataChanged` method. 173 | *
174 |      *     
175 |      *         contextElement.dataChanged = (data) => console.log("data changed");
176 |      *     
177 |      * 
178 | * @param dataSetter 179 | */ 180 | protected updateDataCallback = (dataSetter: DataSetter) => { 181 | this.setData(dataSetter); 182 | const dataChangedEvent: string = composeChangeEventName('data'); 183 | if (dataChangedEvent in this) { 184 | (this as any)[dataChangedEvent].call(this, this.contextData); 185 | } 186 | }; 187 | 188 | /** 189 | * To bubble child action to the parent. 190 | * @param action 191 | */ 192 | protected bubbleChildAction = (action: ArrayAction | Action) => { 193 | const childAction: ChildAction = { 194 | event: action.event, 195 | type: action.type, 196 | childActions: [this.actionToPath(action as ArrayAction)] 197 | }; 198 | this.dispatchDetailEvent(childAction); 199 | }; 200 | 201 | /** 202 | * Updating current data from child action 203 | * @param action 204 | * @param currentAction 205 | */ 206 | protected updateDataFromChild = (action: ChildAction, currentAction: ArrayAction) => { 207 | const reducer = this.reducer; 208 | if (hasNoValue(reducer)) { 209 | action.childActions = [this.actionToPath(currentAction), ...action.childActions]; 210 | this.dispatchDetailEvent(action); 211 | }else{ 212 | this.updateDataCallback((oldData: Context) => { 213 | return reducer(oldData, action); 214 | }); 215 | } 216 | 217 | }; 218 | 219 | /** 220 | * render method is invoked by the component when it received a new data-update. 221 | * First it will create DataRenderer object if its not exist. 222 | * DataRenderer require ContextElement cloned template , updateDataCallback, and reducer. 223 | * 224 | * `cloned template` will be used by the DataRenderer as the real node that will be attached to document body. 225 | * `updateDataCallback` will be used by the DataRenderer to inform the ContextElement if there's new data-update performed by user action. 226 | * `reducer` is an function that will return a new copy of the data.Reducer is invoked when there's user action/ 227 | * 228 | * Each time render method is invoked, a new callback to get the latest data (dataGetter) is created and passed to 229 | * DataRenderer render method. 230 | * 231 | */ 232 | protected render = () => { 233 | if (hasNoValue(this.contextData) || hasNoValue(this.template)) { 234 | return; 235 | } 236 | if (hasNoValue(this.renderer)) { 237 | const dataNodes: ChildNode[] = this.template.map(node => node.cloneNode(true)) as ChildNode[]; 238 | this.renderer = new DataRenderer(dataNodes, this.getAsset, this.updateDataCallback, () => this.reducer, this.bubbleChildAction, this.updateDataFromChild); 239 | } 240 | const reversedNodes: Node[] = [...this.renderer.nodes].reverse(); 241 | let anchorNode: Node = document.createElement('template'); 242 | this.append(anchorNode); 243 | for (const node of reversedNodes) { 244 | if (anchorNode.previousSibling !== node) { 245 | this.insertBefore(node, anchorNode); 246 | } 247 | anchorNode = node; 248 | } 249 | const data = this.contextData; 250 | const dataGetter: DataGetter = () => ({data}); 251 | this.renderer.render(dataGetter); 252 | this.lastChild.remove(); 253 | }; 254 | 255 | /** 256 | * initAttribute is the method to initialize ContextElement attribute invoked each time connectedCallback is called. 257 | */ 258 | protected initAttribute = () => { 259 | }; 260 | 261 | /** 262 | * Dispatch child action event. 263 | * @param childAction 264 | */ 265 | private dispatchDetailEvent = (childAction: ChildAction) => { 266 | const event = new CustomEvent(CHILD_ACTION_EVENT, {detail: childAction, cancelable: true, bubbles: true}); 267 | this.dispatchEvent(event); 268 | }; 269 | 270 | /** 271 | * Populate the ContextElement template by storing the node child-nodes into template property. 272 | * Once the child nodes is stored in template property, ContextElement will clear its content by calling this.innerHTML = '' 273 | */ 274 | private populateTemplate = () => { 275 | this.template = Array.from(this.childNodes).filter(noEmptyTextNode()); 276 | this.innerHTML = ''; // we cleanup the innerHTML 277 | }; 278 | 279 | /** 280 | * Get the super context element, this function will lookup to the parentNode which is instanceof ContextElement, 281 | * If the parent node is instance of contextElement then this node will return it. 282 | * 283 | * @param parentNode 284 | */ 285 | private getSuperContextElement = (parentNode: Node): ContextElement => { 286 | if (parentNode instanceof ContextElement) { 287 | return parentNode; 288 | } else if (hasValue(parentNode.parentNode)) { 289 | return this.getSuperContextElement(parentNode.parentNode); 290 | } 291 | return null; 292 | }; 293 | 294 | } 295 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {ArrayContextElement} from "./array-context-element"; 2 | import {ContextElement} from "./context-element"; 3 | import {ARRAY_CONTEXT_ELEMENT_TAG_NAME, CONTEXT_ELEMENT_TAG_NAME} from "./types"; 4 | 5 | customElements.define(ARRAY_CONTEXT_ELEMENT_TAG_NAME, ArrayContextElement); 6 | customElements.define(CONTEXT_ELEMENT_TAG_NAME, ContextElement); 7 | -------------------------------------------------------------------------------- /src/libs/attribute-evaluator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArrayDataGetterValue, 3 | AssetGetter, 4 | BubbleChildAction, 5 | composeChangeEventName, 6 | contains, 7 | DATA_ACTION_ATTRIBUTE, 8 | DATA_ASSET_ATTRIBUTE, 9 | DATA_WATCH_ATTRIBUTE, 10 | DataGetter, 11 | DataGetterValue, 12 | hasNoValue, 13 | hasValue, 14 | ReducerGetter, 15 | UpdateDataCallback 16 | } from "../types"; 17 | import isValidAttribute from "./attribute-validator"; 18 | 19 | 20 | /** 21 | * AttributeEvaluator is a class that stores information about node that have active-attributes. 22 | * The AttributeEvaluator is called by the DataRenderer object when DataRenderer.render is executed. 23 | * 24 | * AttributeEvaluator require the activeNode,dataGetter,updateDataCallback, and the reducer function from the DataRenderer. 25 | * 26 | * When the AttributeEvaluator initiated, the attribute evaluator will extract all the active-attributes from active-node and store them in 27 | * `activeAttributeValue`. 28 | * 29 | * Once the activeAttribute extracted from the node, AttributeEvaluator will remove those attributes from the node, remaining 30 | * only non-active attributes. 31 | * 32 | * The non active attributes then will be extracted from the node, and stored in the `defaultAttributeValue` property. 33 | * 34 | * The next step of the initialization process it to extract the active attributes and group them into 3 different map. 35 | * 1. stateAttributeProperty : mapping of `data.property` group by first state then attribute. 36 | * 2. attributeStateProperty : mapping of `data.property` group by first attribute then state. 37 | * 3. eventStateAction : mapping of action group by first event then state. 38 | * 39 | * The last step of the initialization of AttributeEvaluator, is to bind the node against eventStateAction. 40 | */ 41 | export default class AttributeEvaluator { 42 | 43 | /** 44 | * active-node is the actual HTMLElement attached to the document.body 45 | */ 46 | private readonly activeNode: ChildNode; 47 | 48 | /** 49 | * active-attribute, is a map of node attribute which has either `watch|toggle|action` and its value 50 | */ 51 | private readonly activeAttributeValue: Map; 52 | 53 | /** 54 | * DataGetter is a callback function to get the current actual data. 55 | */ 56 | private readonly dataGetter: DataGetter; 57 | 58 | /** 59 | * AssetGetter is a callback function to get the asset from the context-element 60 | */ 61 | private readonly assetGetter: AssetGetter; 62 | 63 | /** 64 | * DataUpdateCallback is a callback to inform DataRenderer that a new copy of data is available. 65 | */ 66 | private readonly updateData: UpdateDataCallback; 67 | 68 | /** 69 | * Bubble child action is a callback to bubble action to the parent data renderer. 70 | */ 71 | private readonly bubbleChildAction: BubbleChildAction; 72 | 73 | /** 74 | * callback function that is called when an action is triggered by dom event. 75 | */ 76 | private readonly reducerGetter: ReducerGetter; 77 | 78 | // mapping for watch & assets 79 | private readonly attributeProperty: Map> = null; 80 | 81 | 82 | // mapping for action 83 | private readonly eventAction: Map = null; 84 | 85 | /** 86 | * Constructor will perform initialization by constructing activeAttributeValue, defaultAttributeValue, eventStateAction, 87 | * stateAttributeProperty and attributeStateProperty. 88 | * The last process would be initialization of event listener. 89 | * 90 | * @param activeNode : node that contains active-attribute. 91 | * @param assetGetter : callback function to get the asset from context-data 92 | * @param dataGetter : callback function to return current data. 93 | * @param updateData : callback function to inform DataRenderer that a new data is created because of user action. 94 | * @param reducerGetter : function to map data into a new one because of user action. 95 | * @param activeAttributes : attributes that is used to lookup the nodes 96 | * @param bubbleChildAction 97 | */ 98 | constructor(activeNode: ChildNode, assetGetter: AssetGetter, dataGetter: DataGetter, updateData: UpdateDataCallback, reducerGetter: ReducerGetter, activeAttributes: string[], bubbleChildAction: BubbleChildAction) { 99 | this.activeNode = activeNode; 100 | this.dataGetter = dataGetter; 101 | this.assetGetter = assetGetter; 102 | this.updateData = updateData; 103 | this.bubbleChildAction = bubbleChildAction; 104 | this.reducerGetter = reducerGetter; 105 | this.activeAttributeValue = populateActiveAttributeValue(activeNode as HTMLElement, activeAttributes); 106 | this.eventAction = mapEventStateAction(this.activeAttributeValue); 107 | this.attributeProperty = mapAttributeProperty(this.activeAttributeValue, [DATA_WATCH_ATTRIBUTE, DATA_ASSET_ATTRIBUTE]); 108 | initEventListener(activeNode as HTMLElement, this.eventAction, dataGetter, updateData, reducerGetter, bubbleChildAction); 109 | } 110 | 111 | /** 112 | * Render method will be invoked my DataRenderer.render. Render method will perform 2 major things, 113 | * update active-attribute `watch:updateAttributeWatch` and `toggle:updateToggleAttribute`. 114 | */ 115 | public render = () => { 116 | const element = this.activeNode as any; 117 | const stateAttributeProperty = this.attributeProperty; 118 | const dataGetterValue = this.dataGetter(); 119 | const data: any = dataGetterValue.data; 120 | const assetGetter = this.assetGetter; 121 | updateWatchAttribute(element, stateAttributeProperty, data,assetGetter); 122 | } 123 | } 124 | 125 | /** 126 | * mapEventStateAction is a function to convert `action` active-attribute to action group by first event, then state. 127 | * @param attributeValue is an active attribute 128 | */ 129 | const mapEventStateAction = (attributeValue: Map) => { 130 | const eventStateAction: Map = new Map(); 131 | attributeValue.forEach((value, attributeName) => { 132 | if (attributeName.endsWith(DATA_ACTION_ATTRIBUTE)) { 133 | const attributes = attributeName.split('.'); 134 | let event = ''; 135 | if (attributes.length === 1) { 136 | event = 'click'; 137 | }else{ 138 | event = attributes[0]; 139 | } 140 | eventStateAction.set(event, value); 141 | } 142 | }); 143 | return eventStateAction; 144 | }; 145 | 146 | /** 147 | * mapStateAttributeProperty is a function to convert `watch` active-attribute to property group by first state, then attribute. 148 | * @param attributeValue 149 | * @param attributePrefixes 150 | */ 151 | const mapAttributeProperty = (attributeValue: Map, attributePrefixes: string[]) => { 152 | const attributeProperty: Map> = new Map>(); 153 | attributeValue.forEach((value, attributeName) => { 154 | if (attributePrefixes.filter(attributePrefix => attributeName.endsWith(attributePrefix)).length > 0) { 155 | const attributes = attributeName.split('.'); 156 | let attribute = ''; 157 | let type = ''; 158 | if (attributes.length === 1) { 159 | attribute = 'content'; 160 | type = attributes[0]; 161 | }else{ 162 | attribute = attributes[0]; 163 | type = attributes[1] 164 | } 165 | if (!attributeProperty.has(attribute)) { 166 | attributeProperty.set(attribute, new Map()); 167 | } 168 | attributeProperty.get(attribute).set(type, value); 169 | } 170 | }); 171 | return attributeProperty; 172 | }; 173 | 174 | 175 | /** 176 | * populateActiveAttributeValue will extract the active-attributes from the element. 177 | * @param element 178 | * @param activeAttributes 179 | */ 180 | const populateActiveAttributeValue = (element: HTMLElement, activeAttributes: string[]) => { 181 | const attributeValue: Map = new Map(); 182 | element.getAttributeNames().filter(name => contains(name, activeAttributes)).forEach(attributeName => { 183 | attributeValue.set(attributeName, element.getAttribute(attributeName)); 184 | element.removeAttribute(attributeName); 185 | }); 186 | return attributeValue; 187 | }; 188 | 189 | /** 190 | * InitEventListener is the function to attach `toggle` active-attribute to HTMLElement.addEventListener. 191 | * This method requires htmlElement, eventStateAction, dataGetter, updateData and reducer. 192 | * 193 | * initEventListener will iterate over the eventStateAction map. Based on the event in eventStateAction, the function 194 | * will addEventListener to the element. 195 | * 196 | * When an event is triggered by the element, the eventListener callback will check event.type. 197 | * If the event.type is `submit` then the event will be prevented and propagation stopped. 198 | * 199 | * When element triggered an event, the current data.state will be verified against the eventStateAction. 200 | * If the current data.state is not available in the eventStateAction, then the event will be ignored. 201 | * 202 | * If the current data.state is available in the eventStateAction, or when GlobalState exist in the eventStateAction, then the 203 | * updateData callback will invoked to inform DataRenderer that user is triggering an action. 204 | * 205 | * @param element 206 | * @param eventStateAction 207 | * @param dataGetter 208 | * @param updateData 209 | * @param reducerGetter 210 | * @param bubbleChildAction 211 | */ 212 | const initEventListener = (element: HTMLElement, eventStateAction: Map, dataGetter: DataGetter, updateData: UpdateDataCallback, reducerGetter: ReducerGetter, bubbleChildAction: BubbleChildAction) => { 213 | eventStateAction.forEach((stateAction: string, event: string) => { 214 | event = event.startsWith('on') ? event.substring('on'.length, event.length) : event; 215 | element.addEventListener(event, (event: Event) => { 216 | event.preventDefault(); 217 | event.stopImmediatePropagation(); 218 | const dataGetterValue: DataGetterValue | ArrayDataGetterValue = dataGetter(); 219 | const reducer = reducerGetter(); 220 | const type = stateAction; 221 | let data = dataGetterValue.data; 222 | const action: any = {type, event}; 223 | 224 | if ('key' in dataGetterValue) { 225 | const arrayDataGetterValue = dataGetterValue as ArrayDataGetterValue; 226 | data = arrayDataGetterValue.data; 227 | action.data = data; 228 | action.key = arrayDataGetterValue.key; 229 | action.index = arrayDataGetterValue.index; 230 | } 231 | if (hasNoValue(reducer)) { 232 | bubbleChildAction(action); 233 | }else{ 234 | updateData((oldData) => reducer(oldData, action)); 235 | } 236 | 237 | }) 238 | }); 239 | }; 240 | 241 | /** 242 | * Function to set property of an element, it will check if the attribute is a valid attribute, if its a valid attribute 243 | * then it will set the attribute value, and if the attribute is element property, then the element will be assigned for the attribute. 244 | * 245 | * @param attribute 246 | * @param element 247 | * @param val 248 | * @param data 249 | * @param property 250 | */ 251 | function setPropertyValue(attribute: string, element: any, val: any, data: any, property: string) { 252 | 253 | if (isValidAttribute(attribute) && element.getAttribute(attribute) !== val) { 254 | element.setAttribute(attribute, val); 255 | } 256 | if (attribute in element) { 257 | element[attribute] = val; 258 | if (attribute === 'data') { 259 | element.dataPath = property; 260 | } 261 | const eventName = composeChangeEventName(attribute); 262 | element[eventName] = (val: any) => injectValue(data, property, val); 263 | } 264 | if (attribute === 'content') { 265 | element.innerHTML = val; 266 | } 267 | } 268 | 269 | /** 270 | * UpdateWatchAttribute is a method that will perform update against `watch` active-attribute. 271 | * 272 | * UpdateWatchAttribute will get the current attributeProps from stateAttributeProps based on the data.state. 273 | * It will iterate over the attribute from the attributeProps. 274 | * On each attribute iteration, the method will set the element.attribute based on the value of data[property]. 275 | * 276 | * If the attribute is also a valid element.property, it will set the value of element.property against the 277 | * data[property] value either. 278 | * 279 | * If the attribute value is `content`, the element.innerHTML value will be set against the data[property] value. 280 | * 281 | * @param element : node or also an HTMLElement 282 | * @param stateAttributeProperty : object that store the mapping of property against state and attribute. 283 | * @param data : current value of the data. 284 | * @param dataState : state value of the object. 285 | * @param assetGetter : callback to get the asset of the context element. 286 | */ 287 | const updateWatchAttribute = (element: any, stateAttributeProperty: Map>, data: any, assetGetter: AssetGetter) => { 288 | 289 | const attributeProps = stateAttributeProperty; 290 | if (hasNoValue(attributeProps)) { 291 | return; 292 | } 293 | attributeProps.forEach((typeProperty: Map, attribute: string) => { 294 | const watchProperty = typeProperty.get(DATA_WATCH_ATTRIBUTE); 295 | const assetProperty = typeProperty.get(DATA_ASSET_ATTRIBUTE); 296 | let val = null; 297 | if (hasValue(watchProperty)) { 298 | val = extractValue(data, watchProperty); 299 | } else if (hasValue(assetProperty)) { 300 | val = assetGetter(assetProperty); 301 | } 302 | setPropertyValue(attribute, element, val, data, watchProperty); 303 | }); 304 | }; 305 | 306 | 307 | /** 308 | * Function to extract the value of json from jsonPath 309 | * @param data 310 | * @param prop 311 | */ 312 | const extractValue = (data: any, prop: string) => { 313 | if (hasNoValue(data)) { 314 | return data; 315 | } 316 | try { 317 | const evaluate = new Function('data', `return data.${prop};`); 318 | return evaluate.call(null, data); 319 | } catch (err) { 320 | console.warn(data, err.message); 321 | } 322 | return null; 323 | }; 324 | 325 | 326 | /** 327 | * Function to extract the value of json from jsonPath 328 | * @param data 329 | * @param prop 330 | * @param value 331 | * 332 | */ 333 | const injectValue = (data: any, prop: string, value: any) => { 334 | if (hasNoValue(data)) { 335 | return; 336 | } 337 | try { 338 | const evaluate = new Function('data', 'value', `data.${prop} = value;`); 339 | return evaluate.call(null, data, value); 340 | } catch (err) { 341 | console.warn(err.message); 342 | } 343 | 344 | }; 345 | -------------------------------------------------------------------------------- /src/libs/attribute-validator.spec.ts: -------------------------------------------------------------------------------- 1 | import isValidAttribute from "./attribute-validator"; 2 | 3 | 4 | test('It should validate the validator', () => { 5 | expect(isValidAttribute('data')).toBe(false); 6 | expect(isValidAttribute('reducer')).toBe(false); 7 | expect(isValidAttribute('value')).toBe(true); 8 | expect(isValidAttribute('style')).toBe(true); 9 | expect(isValidAttribute('class')).toBe(true); 10 | }); -------------------------------------------------------------------------------- /src/libs/attribute-validator.ts: -------------------------------------------------------------------------------- 1 | const ignoredAttributes = ['data', 'reducer']; 2 | 3 | /** 4 | * isValidAttribute return if there is active-attribute to be ignore by the ContextElement. 5 | * @param attributeName 6 | */ 7 | export default function isValidAttribute(attributeName: string) { 8 | return ignoredAttributes.indexOf(attributeName) < 0; 9 | }; 10 | -------------------------------------------------------------------------------- /src/libs/data-renderer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | ARRAY_CONTEXT_ELEMENT_TAG_NAME, 4 | ArrayAction, 5 | ArrayDataGetterValue, 6 | AssetGetter, 7 | BubbleChildAction, 8 | CHILD_ACTION_EVENT, 9 | ChildAction, 10 | contains, 11 | CONTEXT_ELEMENT_TAG_NAME, 12 | DATA_ACTION_ATTRIBUTE, 13 | DATA_ASSET_ATTRIBUTE, 14 | DATA_WATCH_ATTRIBUTE, 15 | DataGetter, 16 | ReducerGetter, 17 | UpdateDataCallback, 18 | } from "../types"; 19 | import noEmptyTextNode from "./no-empty-text-node"; 20 | import AttributeEvaluator from "./attribute-evaluator"; 21 | 22 | /** 23 | * DataRenderer is an object that store cloned ContextElement.template and store it in 'nodes' property. 24 | * During initialization, DataRenderer scanned for the active-nodes against nodes property. 25 | * active-nodes are the node that contain active-attributes such as `watch|toggle|action`. 26 | * 27 | * When the active nodes identified, DataRenderer create AttributeEvaluator against each active-node, and store them in 28 | * attributeEvaluators property. 29 | * 30 | * When DataRenderer.render invoked by the ContextElement, DataRenderer iterate all ActiveAttributes and call 31 | * ActiveAttribute.render method. 32 | */ 33 | export default class DataRenderer { 34 | 35 | /** 36 | * The actual nodes attached to the dom 37 | */ 38 | public nodes: ChildNode[]; 39 | /** 40 | * Callback to get the latest ContextElement.data 41 | */ 42 | private dataGetter: DataGetter; 43 | /** 44 | * Collection of AttributeEvaluator. 45 | */ 46 | private readonly attributeEvaluators: AttributeEvaluator[]; 47 | 48 | /** 49 | * Constructor to setup the DataRenderer initialization. 50 | * 51 | * @param nodes is a cloned of ContextElement.template 52 | * @param assetGetter 53 | * @param updateData 54 | * @param reducerGetter 55 | * @param bubbleChildAction 56 | * @param updateDataFromChild 57 | */ 58 | constructor(nodes: ChildNode[], assetGetter: AssetGetter, updateData: UpdateDataCallback, reducerGetter: ReducerGetter, bubbleChildAction: BubbleChildAction, updateDataFromChild: (action: ChildAction, currentAction: Action | ArrayAction) => void) { 59 | this.nodes = nodes; 60 | this.addChildActionEventListener(updateDataFromChild); 61 | const activeAttributes: (string)[] = [DATA_WATCH_ATTRIBUTE, DATA_ACTION_ATTRIBUTE, DATA_ASSET_ATTRIBUTE]; 62 | const activeNodes: ChildNode[] = Array.from(activeNodesLookup(activeAttributes, nodes)); 63 | const dataGetter = () => this.dataGetter(); 64 | this.attributeEvaluators = activeNodes.map(activeNode => new AttributeEvaluator(activeNode, assetGetter, dataGetter, updateData, reducerGetter, activeAttributes, bubbleChildAction)); 65 | } 66 | 67 | /** 68 | * Render with iterate all the AttributeEvaluators and call the AttributeEvaluator.render 69 | * @param getter 70 | */ 71 | public render = (getter: DataGetter) => { 72 | this.dataGetter = getter; 73 | this.attributeEvaluators.forEach((attributeEvaluator: AttributeEvaluator) => attributeEvaluator.render()); 74 | }; 75 | 76 | private addChildActionEventListener(updateDataFromChild: (action: ChildAction, currentAction: Action | ArrayAction) => void) { 77 | this.nodes.forEach((node) => { 78 | node.addEventListener(CHILD_ACTION_EVENT, (event: CustomEvent) => { 79 | if (event.defaultPrevented) { 80 | return; 81 | } 82 | event.stopImmediatePropagation(); 83 | event.stopPropagation(); 84 | event.preventDefault(); 85 | const childAction: ChildAction = event.detail; 86 | const currentData: ArrayDataGetterValue = this.dataGetter() as ArrayDataGetterValue; 87 | const currentAction: ArrayAction = { 88 | index: currentData.index, 89 | event: childAction.event, 90 | type: childAction.type, 91 | data: currentData.data, 92 | key: currentData.key 93 | }; 94 | updateDataFromChild(childAction, currentAction); 95 | }) 96 | }); 97 | } 98 | } 99 | 100 | 101 | /** 102 | * activeNodesLookup will return nodes which has the `active-attributes`. Active attributes are the node attribute that contains attributesSuffix. 103 | * Example of active-attributes value.watch . 104 | *
105 |  *     
106 |  *         
107 | * 108 | *
109 | *
110 | *
111 | * @param attributesSuffix watch|toggle|action 112 | * @param nodes filter 113 | */ 114 | const activeNodesLookup = (attributesSuffix: string[], nodes: ChildNode[]) => { 115 | return nodes.filter(noEmptyTextNode()).reduce((accumulator, node) => { 116 | if (!(node instanceof HTMLElement)) { 117 | return accumulator; 118 | } 119 | const element = node as HTMLElement; 120 | const attributeNames = element.getAttributeNames(); 121 | for (const attribute of attributeNames) { 122 | if (contains(attribute, attributesSuffix)) { 123 | accumulator.add(element); 124 | } 125 | } 126 | if (!contains(element.tagName, [ARRAY_CONTEXT_ELEMENT_TAG_NAME.toUpperCase(), CONTEXT_ELEMENT_TAG_NAME.toUpperCase()])) { 127 | const childrenNodes = activeNodesLookup(attributesSuffix, Array.from(element.childNodes)); 128 | Array.from(childrenNodes).forEach(childNode => accumulator.add(childNode)); 129 | } 130 | return accumulator; 131 | }, new Set()); 132 | }; 133 | -------------------------------------------------------------------------------- /src/libs/error-message.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Error message to show when data.key is missing in context-array 3 | */ 4 | export const arrayContextElementMissingDataKey = () => `'' requires 'data.key' attribute. data-key value should refer to the unique attribute of the data.`; 5 | -------------------------------------------------------------------------------- /src/libs/no-empty-text-node.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to remove empty text node. 3 | */ 4 | export default function noEmptyTextNode(): (node: ChildNode) => (boolean | true) { 5 | return (node: ChildNode) => { 6 | if (node.nodeType === Node.TEXT_NODE) { 7 | return /\S/.test(node.textContent); 8 | } 9 | return true; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/libs/uuid.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Naive approach of uuid, this is just a helper for testing. ContextElement does not use uuid function. 3 | */ 4 | export default function uuid() { 5 | return '_xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 6 | const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 7 | return v.toString(16); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/stories/context-array/input.stories.ts: -------------------------------------------------------------------------------- 1 | import {object, withKnobs} from "@storybook/addon-knobs"; 2 | import "../../index"; 3 | import {useJavascript} from "../useJavascript"; 4 | import {ArrayContextElement} from "../../array-context-element"; 5 | import {ArrayAction} from "../../types"; 6 | 7 | export default {title: 'Context Array', decorators: [withKnobs]}; 8 | 9 | export const input = () => { 10 | const html = ` 11 |

Sample of action in array

12 | 13 |
Your favorite
14 |
`; 15 | 16 | useJavascript(() => { 17 | interface Data { 18 | name: string, 19 | id: number 20 | } 21 | 22 | const el = document.getElementById('myElement') as ArrayContextElement; 23 | 24 | el.data = object('data', [{ 25 | id: 1, 26 | name: 'Javascript' 27 | }, { 28 | id: 2, 29 | name: 'Typescript' 30 | }]); 31 | 32 | el.reducer = (array, action: ArrayAction) => { 33 | let {type, event, index, data} = action; 34 | if (type === 'SET_FAVORITE') { 35 | { 36 | const newData = { 37 | ...data, 38 | name: (event.target as any).value 39 | }; 40 | return [...array.slice(0, index), newData, ...array.slice(index + 1, array.length)] 41 | } 42 | } 43 | return [...array]; 44 | } 45 | }); 46 | return html; 47 | }; 48 | -------------------------------------------------------------------------------- /src/stories/context-array/timer.stories.ts: -------------------------------------------------------------------------------- 1 | import {object, withKnobs} from "@storybook/addon-knobs"; 2 | import {useJavascript} from "../useJavascript"; 3 | import {ArrayContextElement} from "../../array-context-element"; 4 | 5 | export default {title: 'Context Array', decorators: [withKnobs]}; 6 | 7 | export const timer = () => { 8 | const html = ` 9 |

Generate Random Data

10 | 11 |
`; 12 | 13 | useJavascript(() => { 14 | interface Data { 15 | time: string, 16 | id: number 17 | } 18 | 19 | const el = document.getElementById('myElement') as ArrayContextElement; 20 | 21 | el.data = object('data', [{ 22 | id: 1, 23 | time: Math.round(Math.random() * 1000).toFixed() 24 | }, { 25 | id: 2, 26 | time: Math.round(Math.random() * 1000).toFixed() 27 | }]); 28 | setInterval(() => { 29 | el.data = [{ 30 | id: 1, 31 | time: Math.round(Math.random() * 1000).toFixed() 32 | }, { 33 | id: 2, 34 | time: Math.round(Math.random() * 1000).toFixed() 35 | }]; 36 | }, 1000); 37 | }); 38 | return html; 39 | }; 40 | -------------------------------------------------------------------------------- /src/stories/context-element/checkbox.stories.ts: -------------------------------------------------------------------------------- 1 | import {ContextElement} from "../../context-element"; 2 | import {object, withKnobs} from "@storybook/addon-knobs"; 3 | import {useJavascript} from "../useJavascript"; 4 | 5 | export default {title: 'Context Element', decorators: [withKnobs]}; 6 | 7 | export const checkbox = () => { 8 | const html = ` 9 | 13 |

Value of checkbox is :

14 |
`; 15 | 16 | useJavascript(() => { 17 | 18 | interface Data { 19 | isDone: boolean 20 | } 21 | 22 | const el = document.getElementById('myElement') as ContextElement; 23 | el.data = object('Default State', { 24 | isDone: false 25 | }); 26 | el.reducer = (data, action) => { 27 | const {type, event} = action; 28 | if (type === 'TOGGLE_CHECKBOX') { 29 | { 30 | const isDone = (event.target as HTMLInputElement).checked; 31 | return {...data, isDone} 32 | } 33 | } 34 | return {...data} 35 | } 36 | }); 37 | return html; 38 | }; -------------------------------------------------------------------------------- /src/stories/context-element/input.stories.ts: -------------------------------------------------------------------------------- 1 | import {ContextElement} from "../../context-element"; 2 | import {withKnobs} from "@storybook/addon-knobs"; 3 | import {useJavascript} from "../useJavascript"; 4 | 5 | export default {title: 'Context Element', decorators: [withKnobs]}; 6 | 7 | export const input = () => { 8 | const html = ` 9 | 10 | 11 | `; 12 | 13 | useJavascript(() => { 14 | interface Data { 15 | name: string 16 | } 17 | 18 | const el = document.getElementById('myElement') as ContextElement; 19 | el.data = { 20 | name: 'This is example of binding' 21 | }; 22 | el.reducer = (data, action) => { 23 | const {event, type} = action; 24 | if (type === 'SET_NAME') { 25 | { 26 | const name = (event.target as HTMLInputElement).value; 27 | return {...data, name} 28 | } 29 | } 30 | return {...data} 31 | } 32 | }); 33 | return html; 34 | }; 35 | -------------------------------------------------------------------------------- /src/stories/context-element/timer.stories.ts: -------------------------------------------------------------------------------- 1 | import {ContextElement} from "../../context-element"; 2 | import {object, withKnobs} from "@storybook/addon-knobs"; 3 | import {useJavascript} from "../useJavascript"; 4 | 5 | export default {title: 'Context Element', decorators: [withKnobs]}; 6 | 7 | export const timer = () => { 8 | const html = ` 9 | 10 | `; 11 | 12 | useJavascript(() => { 13 | interface Data { 14 | time: string 15 | } 16 | 17 | const el = document.getElementById('myElement') as ContextElement; 18 | el.data = object('data', { 19 | time: new Date().toLocaleTimeString() 20 | }); 21 | setInterval(() => { 22 | el.data = { 23 | time: new Date().toLocaleTimeString() 24 | } 25 | }, 1000); 26 | }); 27 | return html; 28 | }; 29 | -------------------------------------------------------------------------------- /src/stories/todo.stories.ts: -------------------------------------------------------------------------------- 1 | import {ContextElement} from "../context-element"; 2 | import {withKnobs} from "@storybook/addon-knobs"; 3 | import {useJavascript} from "./useJavascript"; 4 | import uuid from "../libs/uuid"; 5 | 6 | export default {title: 'Todo App', decorators: [withKnobs]}; 7 | 8 | export const todo = () => { 9 | useJavascript(javascript); 10 | return useHtml(); 11 | }; 12 | 13 | 14 | const useHtml = () => ` 15 | 48 | 49 |
50 | 51 |
52 | 53 |
54 | 58 | highlight_off 59 |
60 | 61 |
62 |
63 | `; 64 | 65 | 66 | const todoItemReducer = (array: any, action: any) => { 67 | const {data, type, event, index} = action; 68 | 69 | switch (type) { 70 | case 'SET_DONE' : { 71 | const isDone = (event.target as HTMLInputElement).checked; 72 | const newData = {...data, isDone, _state: isDone ? 'done' : ''}; 73 | return [...array.slice(0, index), newData, ...array.slice(index + 1, array.length)]; 74 | } 75 | case 'DELETE_TODO' : { 76 | return [...array.filter((item: any, itemIndex: number) => index !== itemIndex)]; 77 | } 78 | } 79 | return [...array] 80 | }; 81 | 82 | const mainReducer = (data: any, action: any) => { 83 | const {type, event} = action; 84 | switch (type) { 85 | case 'SET_TODO' : { 86 | const newTodo = {...data.todo, todo: event.target.value}; 87 | return {...data, todo: newTodo} 88 | } 89 | case 'ADD_TODO' : { 90 | const todo = data.todo; 91 | const newTodo = {id: uuid(), todo: '', done: false}; 92 | return {...data, todo: newTodo, todos: [...data.todos, todo]} 93 | } 94 | } 95 | return {...data}; 96 | }; 97 | 98 | const javascript = () => { 99 | const app = document.getElementById('app') as ContextElement; 100 | let DEFAULT_CONTEXT = { 101 | todo: { 102 | id: uuid(), 103 | todo: '', 104 | done: false 105 | }, 106 | todos: Array.from([]), 107 | todoItemReducer 108 | }; 109 | app.data = DEFAULT_CONTEXT; 110 | app.reducer = mainReducer; 111 | }; -------------------------------------------------------------------------------- /src/stories/useJavascript.ts: -------------------------------------------------------------------------------- 1 | export const useJavascript = (callback: () => void) => requestAnimationFrame(callback); -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Action { 2 | type: string; 3 | event: Event; 4 | } 5 | 6 | export interface ArrayAction extends Action { 7 | data: T; 8 | index: number; 9 | key: string; 10 | } 11 | 12 | 13 | export interface ActionPath { 14 | path: string; 15 | key?: string; 16 | index?: number; 17 | data?: any; 18 | } 19 | 20 | export interface ChildAction extends Action { 21 | childActions: ActionPath[]; 22 | } 23 | 24 | export type Reducer = (data: T, action: Action | ArrayAction | ChildAction) => T; 25 | export type ReducerGetter = () => Reducer; 26 | export type Renderer = { render: (dataGetter: () => any) => void, nodes: ChildNode[] }; 27 | export type DataSetter = (oldData: T) => T; 28 | export type ToString = (data: T) => string; 29 | export type DataGetter = () => DataGetterValue | ArrayDataGetterValue; 30 | export type AssetGetter = (key: string) => any; 31 | 32 | export interface DataGetterValue { 33 | data: T; 34 | } 35 | 36 | export interface ArrayDataGetterValue extends DataGetterValue { 37 | key: string; 38 | index: number; 39 | } 40 | 41 | 42 | export type UpdateDataCallback = (value: DataSetter) => void; 43 | export type BubbleChildAction = (action: Action | ArrayAction) => void; 44 | export const composeChangeEventName = (attribute: any) => `${attribute}Changed`; 45 | export const hasValue = (param: any) => param !== undefined && param !== null && param !== ''; 46 | export const hasNoValue = (param: any) => !hasValue(param); 47 | export const contains = (text: string, texts: string[]) => texts.reduce((acc, txt) => acc || text.indexOf(txt) >= 0, false); 48 | 49 | export const DATA_WATCH_ATTRIBUTE = 'watch'; 50 | export const DATA_ACTION_ATTRIBUTE = 'action'; 51 | export const DATA_ASSET_ATTRIBUTE = 'asset'; 52 | 53 | export const DATA_KEY_ATTRIBUTE = 'data.key'; 54 | export const HIDE_CLASS: string = "data-element-hidden"; 55 | export const ARRAY_CONTEXT_ELEMENT_TAG_NAME = 'context-array'; 56 | export const CONTEXT_ELEMENT_TAG_NAME = 'context-element'; 57 | export const CHILD_ACTION_EVENT = 'childAction'; 58 | 59 | const style = document.createElement('style'); 60 | style.innerHTML = `.${HIDE_CLASS} {display: none !important;}`; 61 | document.head.appendChild(style); 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "removeComments": false, 5 | "preserveConstEnums": true, 6 | "sourceMap": true, 7 | "target": "ES6", 8 | "esModuleInterop": true, 9 | "moduleResolution": "node", 10 | "lib": [ 11 | "DOM" 12 | ] 13 | }, 14 | "include": [ 15 | "./**/*" 16 | ], 17 | "exclude": [ 18 | "node_modules", 19 | "dist", 20 | "coverage", 21 | "**/*.spec.ts" 22 | ] 23 | } 24 | --------------------------------------------------------------------------------