├── .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 | [](https://travis-ci.org/marsa-emreef/context-element)
4 | 
5 | [](https://codecov.io/gh/marsa-emreef/context-element)
6 | 
7 | [](https://github.com/semantic-release/semantic-release)
8 | 
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 . 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 : ```Proceed ```
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 |
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 \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 \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 \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 \r\n If you click checkbox the action will call reducer function\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 \n If you click checkbox the action will call reducer function\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 \r\n If you click checkbox the action will call reducer function\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 \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 \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 \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 |
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 |
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 | *
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 | Hello
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 | Hello
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 | *
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 |
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 |
10 |
11 | If you click checkbox the action will call reducer function
12 |
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 |
52 |
53 |
54 |
55 |
56 |
57 |
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 |
--------------------------------------------------------------------------------