├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── component ├── index-dev-component.html └── src │ └── main │ └── scala │ └── im │ └── vdom │ └── component │ ├── Component.scala │ ├── Test.scala │ └── package.scala ├── js └── src │ ├── main │ ├── resources │ │ ├── index-dev-delegate.html │ │ ├── index-dev-todo.html │ │ └── index-dev.html │ └── scala │ │ └── im │ │ ├── events │ │ └── Delegate.scala │ │ └── vdom │ │ ├── FunctionAttributes.scala │ │ ├── Test.scala │ │ ├── backend │ │ └── dom │ │ │ ├── AttributeComponent.scala │ │ │ ├── DOMBackend.scala │ │ │ ├── DOMCleanupActions.scala │ │ │ ├── DOMPatchesComponent.scala │ │ │ ├── DOMUtils.scala │ │ │ ├── DelegateComponent.scala │ │ │ └── RenderToDOMComponent.scala │ │ └── package.scala │ └── test │ └── scala │ └── im │ ├── events │ └── DelegateSpec.scala │ └── vdom │ ├── CleanupSpec.scala │ ├── PatchApplicableSpec.scala │ ├── RenderSpec.scala │ └── backend │ └── dom │ └── DOMUtilsSpec.scala ├── jvm └── src │ ├── main │ └── scala │ │ └── im │ │ └── vdom │ │ └── ScalaXml.scala │ └── test │ └── scala │ └── im │ └── vdom │ ├── PatchSpec.scala │ ├── ScalaXmlSpec.scala │ └── backend │ └── RenderMarkupSpec.scala ├── project ├── Dependencies.scala ├── build.properties └── plugins.sbt ├── reactive ├── index-dev-reactive.html └── src │ └── main │ └── scala │ └── im │ └── vdom │ └── reactive │ ├── Test.scala │ └── package.scala └── shared └── src ├── main └── scala │ └── im │ └── vdom │ ├── Action.scala │ ├── Attributes.scala │ ├── Diff.scala │ ├── Keys.scala │ ├── Patch.scala │ ├── VNode.scala │ ├── backend │ ├── Backend.scala │ ├── Hints.scala │ ├── MarkupBackend.scala │ └── Utils.scala │ └── package.scala └── test └── scala └── im └── vdom ├── DiffSpec.scala ├── OptionSpec.scala ├── VNodeSpec.scala └── backend ├── BackendSpec.scala ├── MarkupBackendSpec.scala └── UtilsSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | build 3 | bin 4 | *~ 5 | .project 6 | .settings 7 | .classpath 8 | .idea 9 | .cache* 10 | *.iml 11 | *.sjsir 12 | *.class 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import Dependencies._ 2 | 3 | name := "scala-vdom root project" 4 | 5 | resolvers := allResolvers 6 | 7 | lazy val commonSettings = Seq( 8 | organization := "org.im.vdom", 9 | version := "0.1.1-SNAPSHOT", 10 | scalaVersion := "2.11.7" 11 | ) 12 | 13 | lazy val commonScalacOptions = Seq("-Xlint", "-deprecation", "-Xfatal-warnings", "-feature") 14 | 15 | 16 | lazy val root = (project in file(".")). 17 | aggregate(vdomJS, vdomJVM, component, reactive). 18 | settings(name := "scala-vdom"). 19 | settings( 20 | publish := {}, 21 | publishLocal := {}) 22 | 23 | // crossProject is a Project builder, not a project unto itself 24 | lazy val vdom = crossProject.in(file(".")). 25 | settings(scalacOptions ++= commonScalacOptions). 26 | settings(commonSettings: _*). 27 | settings(libraryDependencies ++= Seq("org.scalatest" %%% "scalatest" % "3.0.0-M15" % "test")). 28 | 29 | jvmSettings(libraryDependencies ++= Seq( 30 | "org.scala-lang.modules" %% "scala-xml" % "1.0.5" % "compile", 31 | "org.scalacheck" %% "scalacheck" % "1.12.5" % "test" 32 | )). 33 | 34 | jsSettings( 35 | relativeSourceMaps := true, 36 | jsDependencies += RuntimeDOM % "test", 37 | libraryDependencies ++= Seq("org.scala-js" %%% "scalajs-dom" % "latest.release"), 38 | persistLauncher := true, 39 | scalaJSStage in Global := FastOptStage 40 | ) 41 | 42 | lazy val vdomJVM = vdom.jvm 43 | lazy val vdomJS = vdom.js 44 | 45 | lazy val component = (project in file("component")). 46 | dependsOn(vdomJS). 47 | settings(commonSettings: _*). 48 | enablePlugins(ScalaJSPlugin). 49 | settings( 50 | persistLauncher := true, 51 | scalaJSStage in Global := FastOptStage) 52 | 53 | lazy val reactive = (project in file("reactive")). 54 | dependsOn(vdomJS). 55 | settings(commonSettings: _*). 56 | settings(libraryDependencies += "org.monifu" %%% "monifu" % "latest.release"). 57 | enablePlugins(ScalaJSPlugin). 58 | settings( 59 | persistLauncher := true, 60 | scalaJSStage in Global := FastOptStage) 61 | 62 | -------------------------------------------------------------------------------- /component/index-dev-component.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | scalajs-vdom test page 21 | 22 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 |
42 | test10:
43 |
44 |
48 | 50 | 52 | 54 | 55 | -------------------------------------------------------------------------------- /component/src/main/scala/im/vdom/component/Component.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package component 19 | 20 | import _root_.org.scalajs.{dom => d} 21 | 22 | /** 23 | * Basic state requires a DOM node and a virtual tree node (VNode). 24 | */ 25 | trait State { 26 | val node: d.Node 27 | val tree: VNode 28 | } 29 | 30 | /** 31 | * Component type that links together a method to create virtual trees, 32 | * the native DOM node (the peer) and other parts needed for managing 33 | * rendering. 34 | */ 35 | trait Component[S <: State] extends (S => (VNode, S)) { 36 | def map(f: VNode => VNode) = 37 | (state: S) => { 38 | val (vnode, newState) = this(state) 39 | (f(vnode), newState) 40 | } 41 | def flatMap(f: VNode => Component[S]) = 42 | (state: S) => { 43 | val (vnode, newState) = this(state) 44 | f(vnode)(newState) 45 | } 46 | } 47 | 48 | object Component { 49 | 50 | /** 51 | * Easily create a state from a function. 52 | */ 53 | def apply[S <: State](r: S => (VNode, S)): Component[S] = 54 | new Component[S] { 55 | def apply(s: S) = r(s) 56 | } 57 | 58 | protected[this] def internalGetID(node: d.Node): Option[NodeIDType] = { 59 | node match { 60 | case e: d.Element => Some(e.getAttribute(ID_ATTRIBUTE_NAME)) 61 | case _ => None 62 | } 63 | } 64 | 65 | /** 66 | * Get the NodeID for a DOM node. Ensure that caches are in sync 67 | * if `node` has a NodeID but it has not been cached yet. 68 | */ 69 | def getID(node: d.Node): Option[NodeIDType] = { 70 | // extract from node 71 | val id = internalGetID(node) 72 | 73 | // find in cache 74 | val cachedNode = for { 75 | _id <- id 76 | cachedNode <- nodeCache.get(_id) 77 | } yield cachedNode 78 | 79 | id 80 | } 81 | 82 | // 83 | // A variety of caches, for fast lookup 84 | // 85 | val nodeCache: collection.mutable.Map[NodeIDType, Component[_]] = 86 | collection.mutable.Map() 87 | val instancesByRootID: collection.mutable.Map[Int, Component[_]] = 88 | collection.mutable.Map() 89 | val containerByRootID: collection.mutable.Map[Int, d.Node] = 90 | collection.mutable.Map() 91 | 92 | /** 93 | * Register the container. Create root ID if needed. 94 | */ 95 | def registerContainer(container: d.Node): Unit = { 96 | } 97 | 98 | /** 99 | * Render a component into a container. 100 | */ 101 | def render(component: Component[_], container: d.Node): Unit = { 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /component/src/main/scala/im/vdom/component/Test.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package component 19 | 20 | import scala.concurrent.duration._ 21 | import scala.scalajs.js 22 | import scala.scalajs.js.JSApp 23 | import scala.scalajs.js.timers._ 24 | import scala.concurrent.{ Future, ExecutionContext } 25 | import scala.scalajs.concurrent.JSExecutionContext.Implicits.runNow 26 | import _root_.org.scalajs.{dom => d} 27 | import d.document 28 | 29 | import VNode._ 30 | import HTML5Attributes._ 31 | import Styles._ 32 | 33 | object Test extends JSApp { 34 | 35 | def main(): Unit = { 36 | println("test of scala-vdom component") 37 | 38 | case class CountState(val count: Int, val node: d.Node, val tree: VNode) extends State 39 | 40 | val comp = Component[CountState] { state => 41 | val newCount = state.count + 1 42 | val render = tag("div", tag("button", 43 | text(s"Click Me - $newCount"), text(s"You have clicked the button $newCount times!"))) 44 | (render, state.copy(tree = render, count = newCount)) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /component/src/main/scala/im/vdom/component/package.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | package object component { 20 | 21 | type NodeIDType = String 22 | val ID_ATTRIBUTE_NAME = "data-vdom-id" 23 | } -------------------------------------------------------------------------------- /js/src/main/resources/index-dev-delegate.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | scalajs-vdom test page 21 | 22 | 35 | 36 | 37 | 38 |

Pure scala event delegate.

39 | Delegate is derived from the ftdelegate source. The source for 40 | ftdelegate is 41 | ftdelegate on 42 | github. 43 |
44 | 45 | 46 | 47 | 48 | 49 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 75 | 85 | 86 | 87 | 88 | 95 | 96 | 97 | 98 |
Test1 - Button click delegate. Look on console for output
50 |
51 | 52 |
53 |
Test2 - Mouse over event
58 |
59 |

This is text that you can mouse over!

60 |

Feel free to mouse over it!

61 |
62 |
test 3 - re-root delegate
68 |
70 | 72 |
76 |
78 | Parent div
79 |

Child div 80 | - Mouse over me

81 |
By mousing over the child, the mouse over on the parent 82 | shold fire 83 |
84 |
89 |
90 | Test queue cleanup actions. Should print cleanup to console when you press 91 | the button.
92 |
93 |
94 |
99 |
100 | 101 | 102 | 104 | 106 | 107 | -------------------------------------------------------------------------------- /js/src/main/resources/index-dev-todo.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | scalajs-vdom test page - TODO application 21 | 22 | 24 | 25 | 26 | 27 |

The ToDo Application

28 |

Add your todo by typing in the text then pressing Return to 29 | add it to the list of todos.

30 |
31 | 32 | 33 | 35 | 37 | 38 | -------------------------------------------------------------------------------- /js/src/main/resources/index-dev.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | scalajs-vdom test page 21 | 22 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | 49 | 50 | 52 | 53 | 54 | 57 | 58 | 59 | 67 | 68 | 69 | 70 | 74 | 75 | 76 | 80 | 81 | 82 | 83 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 97 | 98 | 99 | 100 | 105 | 106 | 107 | 108 | 112 | 113 | 114 | 115 | 119 |

test1

test2: this line should remain and a 47 | new node added
test3: the original test3 node was 51 | replaced
test4 this line 55 | should remain and two new lines of content will be added with 56 | different styling.
60 |

61 | test5 - testing diff that inserts a new child then replaces the 62 | child. If this line is all you see for test5, then the test 63 | failed.
Below, you should see some test5 lines that 64 | indicate success. 65 |

66 |
/div>
71 |
test5a - testing patching using patch paths
72 |
73 |
77 |
test5b - testing patching using patch paths
78 |
79 |
84 |
85 |

test6 - testing diff when key says its the same VNode

86 |
You should not see this line. If you do its an 87 | error.
88 |
test8 - add an SVG rect
96 |
101 | test9: 102 | 103 |
Look at js console to see output. 104 |
109 | test10:
110 |
111 |
116 | This tests some simple rendering using functional approaches
117 |
118 |
120 | 121 |
122 | 123 | 124 | 129 | 130 |
125 |
126 | 127 |
128 |
131 |
132 | 133 | 134 |
135 |

Render to HTML Markup Tests

136 |
137 | 138 | 139 |
140 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /js/src/main/scala/im/events/Delegate.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package events 18 | 19 | import scalajs.js 20 | import _root_.org.scalajs.dom 21 | import scala.language._ 22 | 23 | /** 24 | * When processing events using a Delegate, Matcher is used to determine if the 25 | * delegate function should be called on the node in the traversal 26 | * from the node where the event occurred to the Delegate's root. 27 | * Return true to allow a Delegate to process the current node in the 28 | * traversal. 29 | */ 30 | trait Matcher extends Function2[dom.EventTarget, dom.EventTarget, Boolean] { 31 | def and(rhs: Matcher) = AndMatch(this, rhs) 32 | def &&(rhs: Matcher) = and(rhs) 33 | def or(rhs: Matcher) = OrMatch(this, rhs) 34 | def ||(rhs: Matcher) = or(rhs) 35 | def not = NotMatch(this) 36 | def unary_! = not 37 | } 38 | 39 | private[events] case class NotMatch(matcher: Matcher) extends Matcher { 40 | def apply(root: dom.EventTarget, current: dom.EventTarget) = !matcher(root, current) 41 | } 42 | 43 | private[events] case class AndMatch(lhs: Matcher, rhs: Matcher) extends Matcher { 44 | def apply(root: dom.EventTarget, current: dom.EventTarget) = lhs(root, current) && rhs(root, current) 45 | } 46 | 47 | private[events] case class OrMatch(lhs: Matcher, rhs: Matcher) extends Matcher { 48 | def apply(root: dom.EventTarget, current: dom.EventTarget) = lhs(root, current) || rhs(root, current) 49 | } 50 | 51 | /** 52 | * Convenience constructors. 53 | */ 54 | object Matcher { 55 | 56 | def apply(f: Function2[dom.EventTarget, dom.EventTarget, Boolean]) = 57 | new Matcher { def apply(root: dom.EventTarget, current: dom.EventTarget) = f(root, current) } 58 | 59 | /** 60 | * Match nothing. 61 | */ 62 | val NoMatch = Matcher { (_, _) => false } 63 | 64 | /** 65 | * Match everything. 66 | */ 67 | val MatchAll = Matcher { (_, _) => true } 68 | 69 | /** 70 | * Match on the tag. Current node must be an Element. 71 | */ 72 | def MatchTag(tagName: String) = Matcher { 73 | (_, _) match { 74 | case (_, el: dom.Element) if (el.tagName.toLowerCase == tagName.toLowerCase) => true 75 | case _ => false 76 | } 77 | } 78 | 79 | /** 80 | * True if the current node is the root. 81 | */ 82 | def MatchRoot = Matcher { (root, target) => root == target } 83 | 84 | /** 85 | * Polyfill but not quite as robust as the reference below. 86 | * 87 | * @see [matches](https://developer.mozilla.org/en-US/docs/Web/API/Element/matches) 88 | */ 89 | private[this] def matches(el: dom.EventTarget, selector: String): Boolean = { 90 | val ns = dom.document 91 | val qmatches = ns.querySelectorAll(selector) 92 | for (i <- 0 until qmatches.length) 93 | if (qmatches(i) == el) return true 94 | false 95 | } 96 | 97 | /** 98 | * True if the potential target matches a selector. 99 | * 100 | * @see [HTML selectors](http://www.w3.org/TR/CSS21/selector.html%23id-selectors) 101 | */ 102 | def MatchSelector(selector: String) = Matcher { (_, el: dom.EventTarget) => matches(el, selector) } 103 | 104 | /** 105 | * True if the target node is an Element and has a matching id. 106 | */ 107 | def MatchId(id: String) = Matcher { 108 | (_, _) match { 109 | case (_, el: dom.Element) => el.id == id 110 | case _ => false 111 | } 112 | } 113 | } 114 | 115 | /** 116 | * A listener receives a dom.Event and the current Node that 117 | * is allowed to process the event. Return true if the propagation up 118 | * the tree should continue and false if it should stop. The handler 119 | * is a scala function because the scala machinery eventually 120 | * retrieves the handler and executes it. 121 | * 122 | */ 123 | trait Handler extends scala.Function2[dom.Event, dom.Node, Boolean] 124 | 125 | /** 126 | * Importing Handler.Implicits into scope brings in some implicits for automatic 127 | * conversion of scala functions to Handler objects. 128 | * 129 | */ 130 | object Handler { 131 | def apply(f: scala.Function2[dom.Event, dom.Node, Boolean]) = new Handler { 132 | def apply(event: dom.Event, node: dom.Node) = f(event, node) 133 | } 134 | 135 | def apply(f: scala.Function1[dom.Event, Boolean]) = new Handler { 136 | def apply(event: dom.Event, node: dom.Node) = f(event) 137 | } 138 | 139 | object Implicits { 140 | implicit def toHandler(f: (dom.Event, dom.Node) => Boolean) = Handler(f) 141 | 142 | implicit def toHandlerUnit(f: (dom.Event, dom.Node) => Unit) = new Handler { 143 | def apply(event: dom.Event, node: dom.Node) = { 144 | f(event, node) 145 | true 146 | } 147 | } 148 | implicit def toHandler1(f: dom.Event => Boolean) = Handler(f) 149 | } 150 | } 151 | 152 | /** 153 | * A handler, a matcher and a capture flag. The matcher and capture flag 154 | * are used to qualify whether a handler is used in processing an event. 155 | * Capture refers to the capturing or bubbling phases of event processing. 156 | */ 157 | private[events] case class QualifiedHandler(handler: Handler, matcher: Matcher = Matcher.MatchRoot, capture: Boolean = false) 158 | 159 | /** 160 | * An object that allows a side-effecting call to `cancel`. 161 | * `delegate` is stuck in there for convienence. 162 | */ 163 | trait Cancelable { 164 | def cancel(): Unit 165 | def delegate(): Delegate 166 | } 167 | 168 | /** 169 | * Delegate all event calls on the root to registered handlers. 170 | * You can change the root object at any time and the handlers 171 | * are properly deregistered/registered. The delegate is *not* 172 | * attached to the DOM element. The calling program should do that 173 | * if desired. A Delegate is mutable so you can add and remove 174 | * handlers for specific event types and change the root 175 | * object--this follows the design pattern in ftdomdelegate. 176 | * 177 | * The approach used is standard logic in java swing programs 178 | * with jgoodies. 179 | * 180 | * @see [UI EVents](http://www.w3.org/TR/DOM-Level-3-Events/#interface-EventListener) 181 | */ 182 | case class Delegate(private[events] var root: Option[dom.EventTarget] = None, 183 | private[events] val handlers: collection.mutable.Map[String, collection.mutable.Set[QualifiedHandler]] = collection.mutable.Map.empty) { 184 | self => 185 | 186 | /** 187 | * Construct with a specific root. 188 | */ 189 | def this(root: dom.EventTarget) = this(Some(root)) 190 | 191 | /** 192 | * Handle an event. This is the universal listener attached 193 | * to the root. A mechanism is in place to not process an event 194 | * when it crosses to a different Delegate instances that may have 195 | * attached handlers further up the tree and the event has been 196 | * marked to be ignored by delegate processing. 197 | * 198 | * Handlers can return a false value to indicate that delegates 199 | * should ignore the event. 200 | * 201 | * @see [eventPhase](https://developer.mozilla.org/en-US/docs/Web/API/Event/eventPhase) 202 | */ 203 | @js.annotation.JSExport 204 | protected def handler(event: dom.Event): Unit = { 205 | 206 | if(root.isEmpty) { 207 | println("Delegate root is empty but a handler is firing") 208 | assert(root.isDefined) 209 | } 210 | 211 | import js.DynamicImplicits.truthValue 212 | 213 | // If a special marker is found, other instances of Delegate 214 | // found up the chain should ignore this event as well. 215 | if (truthValue(event.asInstanceOf[js.Dynamic].__DELEGATEIGNORE)) 216 | return 217 | 218 | var target = 219 | event.target match { 220 | case d: dom.Node if (d.nodeType == 3 && d.parentNode != null) => d.parentNode 221 | case n@_ => n.asInstanceOf[dom.Node] 222 | } 223 | 224 | // Build a listener list to process based on the event type and phase... 225 | // If eventPhase is defined, use it, otherwise, determine it from the targets... 226 | val phase = 227 | if (truthValue(event.eventPhase.asInstanceOf[js.Dynamic])) event.eventPhase 228 | else if (event.target != event.currentTarget) 3 // bubbling 229 | else 2 // at targetfound 230 | 231 | // filter registered handlers based on whether they are for capture and the processing phase. 232 | val registeredHandlers = handlers.getOrElse(event.`type`, Set.empty).filter { qhandler => 233 | if (qhandler.capture && (phase == 1 || phase == 2)) true 234 | else if (!qhandler.capture && (phase == 2 || phase == 3)) true 235 | else false 236 | } 237 | 238 | var cont = true 239 | root.foreach { rt => 240 | while (target != null) { 241 | registeredHandlers.foreach { qhandler => 242 | if (qhandler.matcher(rt, target)) { 243 | cont = qhandler.handler(event, target) 244 | } 245 | if (!cont) { 246 | event.asInstanceOf[js.Dynamic].__DELEGATEIGNORE = true 247 | event.preventDefault 248 | return // ouch! rework logic so this is not needed in the middle 249 | } 250 | } 251 | if (target == rt) 252 | return 253 | target = target.parentNode 254 | } 255 | } 256 | } 257 | 258 | /** 259 | * List of events that by default should be captured versus bubbled. 260 | */ 261 | private[events] val captureList = Seq("abort", "blur", 262 | "error", "focus", "load", 263 | "mouseenter", "mouseleave", 264 | "resize", "scroll", "unload") 265 | 266 | /** 267 | * Whether the event should by default, be processed in the capture phase or not. 268 | */ 269 | protected def isDefaultCapture(eventName: String) = captureList.contains(eventName) 270 | 271 | /** 272 | * Apply this configured Delegate to the root and return a new Delegate. 273 | * You can attach to the root `Document.documentElement` to listen to 274 | * events globally. 275 | */ 276 | def root(el: Option[dom.EventTarget]): Delegate = { 277 | root.foreach(stopListeningTo(_)) 278 | root = el 279 | el.foreach(startListeningTo(_)) 280 | this 281 | } 282 | 283 | protected def stopListeningTo(el: dom.EventTarget): Unit = { 284 | require(el != null) 285 | val stops = collection.mutable.ListBuffer[(String, QualifiedHandler)]() 286 | for { 287 | et <- handlers.keys 288 | setOfQL <- handlers.get(et) 289 | ql <- setOfQL 290 | } { stops += ((et, ql)) } 291 | stops.foreach { p => 292 | el.removeEventListener(p._1, handler _, p._2.capture) 293 | } 294 | } 295 | 296 | protected def startListeningTo(el: dom.EventTarget): Unit = { 297 | require(el != null) 298 | for { 299 | et <- handlers.keys 300 | setOfQL <- handlers.get(et) 301 | ql <- setOfQL 302 | } { 303 | el.addEventListener(et, handler _, ql.capture) 304 | } 305 | } 306 | 307 | /** 308 | * Turn off listening for events for a specific eventType. Individual 309 | * handlers should be cancelled using the Cancelable returned from `on`. 310 | * 311 | * @param eventType The event type or None indicating all handlers for all event types. 312 | */ 313 | def off(eventType: Option[String] = None): Delegate = { 314 | import vdom.OptionOps 315 | var removals: collection.mutable.Set[(String, QualifiedHandler)] = collection.mutable.Set.empty 316 | 317 | // Find removals. 318 | for { 319 | et <- handlers.keys 320 | setOfQL <- handlers.get(et) 321 | ql <- setOfQL 322 | } { 323 | if (Some(et) wildcardEq eventType) { 324 | removals += ((et, ql)) 325 | setOfQL -= ql 326 | } 327 | } 328 | 329 | // Remove event listeners based on removals. 330 | for { r <- removals } root.foreach(_.removeEventListener(r._1, this.handler _, r._2.capture)) 331 | 332 | this 333 | } 334 | 335 | /** 336 | * Add a handler for a specific event. The same handler can be added multiple times. 337 | * 338 | * @return A Cancelable used to cancel the listening of the handler. 339 | */ 340 | def on(eventType: String, 341 | handler: Handler, 342 | matcher: Matcher = Matcher.MatchRoot, 343 | useCapture: Option[Boolean] = None): Cancelable = { 344 | 345 | val capture = useCapture.getOrElse(isDefaultCapture(eventType)) 346 | 347 | // If root defined, add universal handler to root if this is the 348 | // first time that eventType has been requested to monitor. 349 | if (!handlers.contains(eventType)) 350 | root.foreach(_.addEventListener(eventType, this.handler _, capture)) 351 | 352 | // Create new listeners set for a specific event by adding the new delegate. 353 | val qhandler = QualifiedHandler(handler, matcher, capture) 354 | val newHandlers = handlers.getOrElse(eventType, collection.mutable.Set.empty) + qhandler 355 | 356 | // Update the handler set. 357 | handlers += (eventType -> newHandlers) 358 | 359 | new Cancelable { 360 | private val _eventType = eventType 361 | private val _qhandler = qhandler 362 | def cancel(): Unit = { 363 | handlers.get(_eventType).foreach(_.remove(_qhandler)) 364 | if (handlers.get(_eventType).map(_.size).getOrElse(0) > 0) { 365 | // no need to listen to this event type, no handlers 366 | root.foreach(_.removeEventListener(_eventType, self.handler _, capture)) 367 | } 368 | } 369 | def delegate() = self 370 | } 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /js/src/main/scala/im/vdom/FunctionAttributes.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import scala.language._ 20 | 21 | /** 22 | * DOM events. If you can define your own convenience methods for defining methods and import 23 | * those into scope. 24 | */ 25 | trait UIEvents { 26 | implicit class FunctionToKey(eventType: String) extends RichFuncString(eventType) 27 | val abort = "abort".func 28 | val beforeinput = "beforeinput".func 29 | val blur = "blur".func 30 | val click = "click".func 31 | val compositionstart = "compositionstart".func 32 | val compositionupdate = "compositionupdate".func 33 | val compositionend = "compositionend".func 34 | val dblclick = "dblclick".func 35 | val error = "error".func 36 | val focus = "focus".func 37 | val focusin = "focusin".func 38 | val focusout = "focusout".func 39 | val input = "input".func 40 | val keyup = "keyup".func 41 | val keydown = "keydown".func 42 | val load = "load".func 43 | val mousedown = "mousedown".func 44 | val mouseenter = "mouseenter".func 45 | val mouseleave = "mouseleave".func 46 | val mouseout = "mouseout".func 47 | val mouseover = "mouseover".func 48 | val resize = "resize".func 49 | val scroll = "scroll".func 50 | val select = "select".func 51 | val unload = "unload".func 52 | val wheel = "wheel".func 53 | } 54 | 55 | object UIEvents extends UIEvents 56 | 57 | -------------------------------------------------------------------------------- /js/src/main/scala/im/vdom/backend/dom/AttributeComponent.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package backend 19 | package dom 20 | 21 | import scala.concurrent.ExecutionContext 22 | import scala.concurrent.Future 23 | import scala.scalajs.concurrent.JSExecutionContext.queue 24 | import scala.scalajs.js 25 | import scala.scalajs.js._ 26 | import scala.scalajs.js.UndefOr 27 | import scala.scalajs.js.UndefOr._ 28 | import scala.language._ 29 | import _root_.org.scalajs.{dom => d} 30 | import Defaults._ 31 | import im.vdom.StyleKey 32 | import im.vdom.ActionLists 33 | import im.vdom.backend.AttrHint 34 | import im.vdom.AttrKey 35 | 36 | /** 37 | * Allows lifting `KeyValue` pairs into a function that can manage DOM attributes. 38 | * Subclass and override and define a new Backend to use your attribute hints 39 | * and side-effecting functions. 40 | * 41 | * This layer does not automatically run named cleanup actions. 42 | */ 43 | trait AttributeComponent { self: DOMHints with DelegateComponent with ActionLists[d.Node] => 44 | 45 | /** 46 | * Call `Element.setAttribute()` with an optional namespace. 47 | */ 48 | protected def setAttribute(el: d.Element, name: String, 49 | value: String, namespace: Option[String] = None): Unit = { 50 | namespace.fold(el.setAttribute(name, value))(ns => el.setAttributeNS(ns, name, value)) 51 | } 52 | 53 | protected def attr(node: d.Node, kv: KeyValue[_]): Unit = { 54 | val el = node.asInstanceOf[d.Element] 55 | val name = kv.key.name 56 | val hints: AttrHint = attrHint(name).getOrElse(Hints.EmptyAttrHints) 57 | kv.value.fold(el.removeAttribute(name)) { v => 58 | if (hints.values(Hints.MustUseAttribute)) 59 | setAttribute(el, name, v.toString, kv.key.namespace) 60 | else 61 | el.asInstanceOf[js.Dynamic].updateDynamic(name)(v.asInstanceOf[js.Any]) 62 | } 63 | } 64 | 65 | protected def style(node: d.Node, kv: KeyValue[_]): Unit = { 66 | val name = kv.key.name 67 | val style = node.asInstanceOf[d.html.Element].style 68 | kv.value.map(_.toString).fold[Unit](style.removeProperty(name))(v => style.setProperty(name, v, "")) 69 | } 70 | 71 | /** 72 | * Add a `Delegate` to a DOM node, if needed, to hande the kv event handler. If 73 | * the handler is None, removes the event type handler but leaves the Delegate. 74 | * Skips adding the handler if it already has been attached. 75 | * 76 | * An action is added to remove the Delegate as part of a cleanup activity. 77 | */ 78 | protected def handler(node: d.Node, key: FunctionKey, value: Option[FunctionValue]): Unit = { 79 | import events._ 80 | 81 | val name = key.name 82 | 83 | def addit(d: Delegate, v: FunctionValue) = { 84 | val cancelable = d.on(name, v.handler, v.matcher, v.useCapture) 85 | // When this attribute, reperesenting an event, is about to be reset, 86 | // remove this handler so its not registered twice. 87 | addAction(name, node, Action.lift { 88 | cancelable.cancel 89 | node 90 | }) 91 | linkDelegate(d, node) 92 | cancelable 93 | } 94 | 95 | value.fold[Unit] { 96 | getDelegate(node).fold() { d => d.off(Some(name)) } // turn off listening for "name" event 97 | } { v => 98 | getDelegate(node).fold { 99 | val cancelable = addit(Delegate(), v) // create new Delegate 100 | cancelable.delegate.root(Some(node)) // set the node Delegate listens to 101 | } { delegate => 102 | addit(delegate, v).delegate // use existing Delegate 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * Pure side effect. This can be easily mapped or wrapped into an IOAction. 109 | */ 110 | trait KeyValueAction extends (d.Node => Unit) 111 | 112 | /** Lift KeyValue to KeyValueAction */ 113 | implicit def toKeyValueAction(kv: KeyValue[_]): KeyValueAction = { 114 | kv match { 115 | case kv@KeyValue(AttrKey(_, _), _) => new KeyValueAction { 116 | def apply(target: d.Node): Unit = attr(target, kv) 117 | } 118 | case kv@KeyValue(StyleKey(_), _) => new KeyValueAction { 119 | def apply(target: d.Node): Unit = style(target, kv) 120 | } 121 | case kv@KeyValue(key@FunctionKey(_), optv) => new KeyValueAction { 122 | def apply(target: d.Node): Unit = handler(target, key, optv.asInstanceOf[Option[FunctionValue]]) 123 | } 124 | case _ => throw new IllegalArgumentException("Unknown KeyValue KeyPart") 125 | } 126 | } 127 | } 128 | 129 | -------------------------------------------------------------------------------- /js/src/main/scala/im/vdom/backend/dom/DOMBackend.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package backend 19 | package dom 20 | 21 | import scala.concurrent.ExecutionContext 22 | import scala.concurrent.Future 23 | import scala.scalajs.concurrent.JSExecutionContext.queue 24 | import scala.scalajs.js 25 | import scala.scalajs.js._ 26 | import scala.scalajs.js.UndefOr 27 | import scala.scalajs.js.UndefOr._ 28 | import scala.language._ 29 | 30 | import _root_.org.scalajs.{dom => d} 31 | 32 | import Defaults._ 33 | 34 | /** 35 | * A base DOM backend that can be extended with specific components as needed. 36 | * This trait defines the context type. 37 | */ 38 | trait BasicDOMBackend extends Backend with DOMUtils { 39 | type This = BasicDOMBackend 40 | type Context = BasicContext 41 | 42 | protected[this] def createContext() = new BasicContext {} 43 | } 44 | 45 | /** 46 | * An example of extending the basic backend with your components. 47 | */ 48 | trait DOMBackend extends BasicDOMBackend 49 | with DOMPatchesComponent 50 | with RenderToDOMComponent 51 | with AttributeComponent 52 | with DOMHints 53 | with DOMCleanupActions 54 | with DelegateComponent 55 | with ActionLists[d.Node] 56 | 57 | /** 58 | * Use this backend when a DOM patching process is desired. 59 | */ 60 | object DOMBackend extends DOMBackend 61 | 62 | 63 | /** 64 | Note: https://groups.google.com/forum/#!topic/scala-js/qlUJWSQ6ccE 65 | 66 | No Brendon is right: `if (this._map)` tests whether `this._map` is falsy. 67 | To test that according to JS semantics, you can use 68 | 69 | js.DynamicImplicits.truthValue(self._map.asInstanceOf[js.Dynamic]) 70 | 71 | which returns false if and only if `self._map` is falsy. This pattern should typically be avoided in Scala code, unless transliterating from JS. 72 | 73 | Cheers, 74 | Sébastien 75 | 76 | */ 77 | -------------------------------------------------------------------------------- /js/src/main/scala/im/vdom/backend/dom/DOMCleanupActions.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package backend 19 | package dom 20 | 21 | import scala.concurrent.ExecutionContext 22 | import scala.concurrent.Future 23 | import scala.scalajs.concurrent.JSExecutionContext.queue 24 | import scala.scalajs.js 25 | import scala.scalajs.js._ 26 | import scala.scalajs.js.UndefOr 27 | import scala.scalajs.js.UndefOr._ 28 | import scala.language._ 29 | 30 | import _root_.org.scalajs.{ dom => d } 31 | 32 | import Defaults._ 33 | 34 | /** 35 | * Cleanup actions specific to a DOM backend. 36 | * 37 | * Add a "queue" to DOM nodes for cleanup actions that are run when a DOM node is 38 | * detached from the environment or an attribute's value is about to be changed. 39 | * 40 | * Cleanup queues are available for the d.Node as well as each 41 | * attibute-named cleanup queues. An attribute's queue are 42 | * run before the attribute is set to a new value and then the attribute 43 | * cleanup queue is cleared. 44 | * 45 | * A node property called __namedCleanupActions is added to the DOM node. 46 | * 47 | * Cleanup queues can be used to disconnect event handlers or 48 | * manager external resources. 49 | * 50 | */ 51 | trait DOMCleanupActions extends ActionLists[d.Node] { self: DOMBackend => 52 | import js.DynamicImplicits.truthValue 53 | 54 | val nodeCleanupQueueName = "__node__" 55 | 56 | type NamedCleanupActions = Map[String, IOAction[_]] 57 | 58 | /** Sets the action as the new named cleanup action. */ 59 | private[this] def setNamed(node: d.Node, namedActions: NamedCleanupActions): Unit = 60 | node.asInstanceOf[js.Dynamic].__namedCleanupActions = namedActions.asInstanceOf[js.Any] 61 | 62 | private[this] def getNamedQueues(node: d.Node): Option[NamedCleanupActions] = { 63 | val x = node.asInstanceOf[js.Dynamic].__namedCleanupActions 64 | if (truthValue(x)) Some(x.asInstanceOf[NamedCleanupActions]) 65 | else None 66 | } 67 | 68 | /** 69 | * Add a cleanup action to run just before an attribute's value is set 70 | * to a new value. The specified actions are run after already registered actions. 71 | */ 72 | def addAction(keyName: String, node: d.Node, action: IOAction[_]*): Unit = { 73 | val actions = Action.seq(action: _*) 74 | val named = getNamedQueues(node).getOrElse(Map()) 75 | val existingAction = named.get(keyName).getOrElse(Action.successful(())) 76 | val newAction = existingAction andThen actions 77 | setNamed(node, named + (keyName -> newAction)) 78 | } 79 | 80 | def addDetachAction(node: d.Node, action: IOAction[_]*): Unit = { 81 | val latest = Action.seq(action: _*) 82 | addAction(nodeCleanupQueueName, node, latest) 83 | } 84 | 85 | /** 86 | * Runs a breadth first pass at running actions. First children recursion, 87 | * then attributes then the node itself. The action list is set to undefined. 88 | */ 89 | def runActions(node: d.Node): Unit = { 90 | getNamedQueues(node).fold() { actionMap => 91 | (0 until node.childNodes.length).foreach{ i => runActions(node.childNodes(i)) } 92 | (actionMap - nodeCleanupQueueName).values.foreach(run(_)) 93 | actionMap.get(nodeCleanupQueueName).foreach(run(_)) 94 | 95 | } 96 | node.asInstanceOf[js.Dynamic].__namedCleanupActions = js.undefined.asInstanceOf[js.Any] 97 | } 98 | 99 | def runActions(name: String, node: d.Node): Unit = { 100 | getNamedQueues(node).fold() { _.get(name).foreach(run(_)) } 101 | // clear only that queue 102 | getNamedQueues(node).foreach { a => setNamed(node, a - name) } 103 | } 104 | 105 | /** 106 | * Create a `d.Node => IOAction[d.Node]` function that can be used to flatMap 107 | * an existing IOAction. When run, it adds a cleanup method to the d.Node's cleanup queue. 108 | * Returns the d.Node input. 109 | */ 110 | private def cleanup(action: IOAction[_]*): (d.Node => IOAction[d.Node]) = 111 | (node: d.Node) => Action.lift { 112 | addDetachAction(node, action: _*) 113 | node 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /js/src/main/scala/im/vdom/backend/dom/DOMPatchesComponent.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package backend 19 | package dom 20 | 21 | import scala.concurrent.ExecutionContext 22 | import scala.concurrent.Future 23 | import scala.scalajs.concurrent.JSExecutionContext.queue 24 | import scala.scalajs.js 25 | import scala.scalajs.js._ 26 | import scala.scalajs.js.UndefOr 27 | import scala.scalajs.js.UndefOr._ 28 | import scala.language._ 29 | 30 | import _root_.org.scalajs.{dom => d} 31 | 32 | import Defaults._ 33 | 34 | 35 | 36 | /** 37 | * DOM specific Patch processing. Includes an implicit to automatically 38 | * convert Patches to PatchPerformers. 39 | * 40 | * In here be dragons. 41 | * 42 | * I'm not sure that we need to link the vnode to the dom node as the 43 | * events delegate and cleanup queues are independent of the vnode and 44 | * the linkage is not needed for those two areas. 45 | */ 46 | trait DOMPatchesComponent extends PatchesComponent { 47 | self: BasicDOMBackend with RenderToDOMComponent with AttributeComponent with ActionLists[d.Node] => 48 | 49 | type PatchInput = d.Node 50 | type PatchOutput = d.Node 51 | 52 | /** An action that unlinks a VNode from a d.Node. Runs cleanup actions. Returns the input d.Node. */ 53 | protected def detach(node: d.Node): IOAction[PatchOutput] = Action.lift { 54 | runActions(node) 55 | DOMEnvironment.unlink(node) 56 | node 57 | } 58 | 59 | /** An action that adds a link between a VNode and d.Node. Returns d.Node. */ 60 | protected def attach(vnode: VNode, dnode: d.Node): IOAction[PatchOutput] = Action.lift { 61 | DOMEnvironment.link(vnode, dnode) 62 | dnode 63 | } 64 | 65 | /** 66 | * Automatically convert a Patch to a PatchPerformer so it can be 67 | * applied easily to an input. 68 | */ 69 | implicit def toPatchPerformer(patch: Patch)(implicit executor: ExecutionContext) = 70 | makeApplyable(patch)(executor) 71 | 72 | /** 73 | * Convert patches to a runnable patch based on the backend then it can be 74 | * converted to an IOAction to run. The PatchPerformer's are constructed 75 | * to return either the target node, the node that was just changed, or the 76 | * node that was created or changed. This allows you to use combinators 77 | * to run actions on the object returned from the operation. 78 | */ 79 | def makeApplyable(patch: Patch)(implicit executor: ExecutionContext): PatchPerformer = { 80 | patch match { 81 | 82 | case PathPatch(patch, path) => 83 | val pp = makeApplyable(patch) 84 | PatchPerformer { target => 85 | find(target, path).fold[IOAction[PatchOutput]]( 86 | Action.failed(new IllegalArgumentException(s"Unable to find node from $target using path $path")))( 87 | newTarget => pp(newTarget)) 88 | } 89 | 90 | case OrderChildrenPatch(i) => PatchPerformer { target => 91 | require(target != null) 92 | // Process removes correctly, don't delete by index, delete by node object. 93 | // I should not need undefor here, a bad index should throw an exception. It's an API violation. 94 | // The node indexes change as you delete which makes this harder than you think. 95 | val childrenToRemove = i.removes.map(target.childNodes(_).asInstanceOf[UndefOr[d.Node]]) 96 | childrenToRemove.foreach { c: UndefOr[d.Node] => 97 | c.foreach { x => 98 | target.removeChild(x) 99 | } 100 | } 101 | // Process moves. 102 | // Make copy of current nodes and move via the indexes. 103 | val copyOfCurrent = (0 to target.childNodes.length).map(target.childNodes(_)) 104 | i.moves.foreach { 105 | case (from, to) => 106 | target.childNodes(to) = copyOfCurrent(from) 107 | } 108 | val x = childrenToRemove.filter(_.isDefined).map(_.get).map(detach(_)) 109 | Action.seq(x: _*) andThen Action.successful(target) 110 | } 111 | 112 | /** 113 | * Run left then right. Return the original input object. 114 | * 115 | * TODO: Should this return, somehow, the output of the left and right operations? 116 | */ 117 | case AndThenPatch(left, right) => { 118 | val pleft = makeApplyable(left) 119 | val pright = makeApplyable(right) 120 | PatchPerformer { target => pleft(target) andThen pright(target) andThen Action.successful(target) } 121 | } 122 | 123 | case KeyValuePatch(keyValues) => PatchPerformer { target => 124 | require(target != null) 125 | val ele = target.asInstanceOf[d.Element] 126 | keyValues.foreach { a => 127 | runActions(a.key.name, target) // run cleanup for that key 128 | a(ele) // then apply the keyvalue to the element to add it back 129 | } 130 | Action.successful(target) 131 | } 132 | 133 | case RemovePatch => PatchPerformer { target => 134 | require(target != null) 135 | target.parentUndefOr.fold[IOAction[PatchOutput]](Action.failed(new IllegalArgumentException(s""))) { p => 136 | p.removeChild(target) 137 | detach(target) 138 | } 139 | } 140 | 141 | case InsertPatch(vnode, pos) => PatchPerformer { target => 142 | require(target != null) 143 | render(vnode).map { newNode => 144 | pos.filter(i => i < target.childNodes.length && i >= 0).fold(target.appendChild(newNode)) { index => 145 | target.insertBefore(newNode, target.childNodes(index)) 146 | } 147 | }.flatMap(attach(vnode, _)) 148 | } 149 | 150 | // Not sure this returns el, but maybe does not matter if text node is always a leaf 151 | case TextPatch(content) => PatchPerformer { el => 152 | require(el != null) 153 | Action.successful { 154 | if (el.nodeType == 3) { 155 | // if already a text node, just replace it 156 | val textEl = el.asInstanceOf[d.Text] 157 | textEl.replaceData(0, textEl.length, content) 158 | //textEl.data = content 159 | textEl 160 | } else { 161 | val t2: UndefOr[d.Text] = createText(content) 162 | replaceRoot(el, t2) 163 | t2.get 164 | } 165 | } 166 | } 167 | 168 | /** 169 | * If a target has a parent, replace target with the 170 | * replacement. Otherwise, throw an exception. Return 171 | * the new node that replaced the target node. 172 | */ 173 | case ReplacePatch(replacement) => PatchPerformer { target => 174 | require(target != null) 175 | // replaceChild returns the old node, not the new one! we want the new one!s 176 | target.parentUndefOr.fold[IOAction[PatchOutput]](Action.failed( 177 | new IllegalArgumentException(s"No parent for target $target"))) { parent => 178 | detach(target) andThen render(replacement).map { n => 179 | parent.replaceChild(n, target) 180 | n 181 | }.flatMap(attach(replacement, _)) 182 | } 183 | } 184 | 185 | /** 186 | * Don't do anything, but pass along the PatchInput object, whatever it was. 187 | */ 188 | case EmptyPatch => PatchPerformer { Action.successful(_) } 189 | } 190 | } 191 | } 192 | 193 | -------------------------------------------------------------------------------- /js/src/main/scala/im/vdom/backend/dom/DOMUtils.scala: -------------------------------------------------------------------------------- 1 | /* Copyright 2015 Devon Miller 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package im 16 | package vdom 17 | package backend 18 | package dom 19 | 20 | import scalajs.js 21 | import js.UndefOr 22 | import _root_.org.scalajs.{ dom => d } 23 | 24 | /** 25 | * Utilities to help smooth through the different ways DOM environments 26 | * handle corner cases. Accessing this module forces some minor 27 | * detection processing to occur. 28 | * 29 | */ 30 | trait DOMUtils { 31 | 32 | import Defaults._ 33 | 34 | private val doc = d.document 35 | 36 | /** 37 | * Create a text node. 38 | */ 39 | def createText(content: String) = doc.createTextNode(content) 40 | 41 | /** 42 | * Create a DOM element using selected information from description. 43 | * This function creates the element but does not perform any other 44 | * initialization processing e.g. setting attributes. 45 | */ 46 | def createEl(description: VirtualElementNode): d.Element = { 47 | description.namespace.fold(doc.createElement(description.tag))(ns => doc.createElementNS(ns, description.tag)) 48 | } 49 | 50 | // val (isWebKit, isFirefox, isTrident) = { 51 | // val uagent = dom.window.navigator.userAgent 52 | // (uagent.contains("WebKit"), uagent.contains("Firefox"), 53 | // uagent.indexOf("Trident")) 54 | // } 55 | 56 | val (normalizes, ignoresEmptyText) = { 57 | // var p: d.Element = dom.document.createElement("p") 58 | // p.appendChild(dom.document.createTextNode("a")) 59 | // p.insertAdjacentHTML("beforeend", "b") 60 | // val normalizes = p.childNodes.length == 1 61 | 62 | // var p = dom.document.createElement("p") 63 | // p.appendChild(dom.document.createTextNode("")) 64 | // p.insertAdjacentHTML("beforeend", "") 65 | // val ignoresEmptyText = p.firstChild.nodeType != 3 66 | // (normalizes, ignoresEmptyText) 67 | (true, true) 68 | } 69 | 70 | /** 71 | * Return falsy value as if calling javascript's `object.member` to see if an object 72 | * has a method or property on it. 73 | */ 74 | def has(node: d.Node, property: String): Boolean = 75 | js.DynamicImplicits.truthValue(node.asInstanceOf[js.Dynamic](property).asInstanceOf[js.Dynamic]) 76 | 77 | /** Remove all children from the node. */ 78 | def removeChildren(node: d.Node): d.Node = { 79 | assert(node != null) 80 | var last: d.Node = node.lastChild 81 | while (last != null) { 82 | node.removeChild(last) 83 | last = node.lastChild 84 | } 85 | node 86 | } 87 | 88 | // /** 89 | // * Replace the oldRoot with the newRoot.There must be a parent to oldRoot otherwise 90 | // * no change is performed. 91 | // */ 92 | // def replaceRoot(oldRoot: d.Node, newRoot: d.Node): Unit = { 93 | // if (oldRoot.parentUndefOr.isDefined) { 94 | // oldRoot.parentNode.replaceChild(newRoot, oldRoot) 95 | // } 96 | // } 97 | 98 | /** 99 | * Replace oldRoot with newRoot. Both must exist and oldRoot must have a parent. 100 | * Implicits may help lift regular nodes into UndefOr. 101 | */ 102 | def replaceRoot(oldRoot: UndefOr[d.Node], newRoot: UndefOr[d.Node]): Unit = { 103 | val x = for { 104 | p <- oldRoot 105 | parent <- p.parentUndefOr 106 | t <- newRoot 107 | } yield parent.replaceChild(t, p) 108 | } 109 | 110 | /** 111 | * Find a node by navigating through the children based on the child indexes. 112 | * Return None if no node is found, and hence, the path was not in alignment 113 | * with the actual child structure. 114 | */ 115 | def find(target: d.Node, path: Seq[Int]): Option[d.Node] = { 116 | path match { 117 | case Nil => Some(target) 118 | case head :: tail => 119 | if (target.childNodes.length == 0 || 120 | head >= target.childNodes.length || 121 | target.childNodes(head) == null) return None 122 | find(target.childNodes(head), path.drop(1)) 123 | } 124 | } 125 | 126 | import util.control.Exception 127 | 128 | /** 129 | * If the element has a custom data attribute storing the checksum and its value 130 | * matches the calculated checksum on the markup input, return true. Otherwise, 131 | * return false. 132 | */ 133 | def canReuseMarkup(markup: String, n: d.Element) = 134 | n.getAttribute(Utils.ChecksumAttrName) match { 135 | case checksumstr: String => 136 | val checksum = Exception.catching(classOf[NumberFormatException]) opt (checksumstr.toInt) getOrElse (-1) 137 | if (checksum == Utils.adler32(markup)) true 138 | else false 139 | case null => false 140 | } 141 | 142 | } 143 | 144 | object DOMUtils extends DOMUtils 145 | 146 | /** 147 | * A set of IOActions that can be composed with other IOActions. 148 | */ 149 | trait DOMActions { 150 | } 151 | 152 | /** @see [DOMAcions] */ 153 | object DOMActions extends DOMActions 154 | 155 | /** 156 | * Link VNodes and DOM Nodes. This can be implemented either in a global map 157 | * or by sticking the vnode into the DOM node using a secret property. The 158 | * secret property is `__vnode`. 159 | * 160 | * Note: truthy test: js.DynamicImplicits.truthValue(self._map.asInstanceOf[js.Dynamic]) 161 | */ 162 | trait DOMInstanceMap { 163 | import events._ 164 | 165 | /** 166 | * Link a VNode to a DOM Node. 167 | */ 168 | def link(vnode: VNode, dnode: d.Node): Unit = { 169 | dnode.asInstanceOf[js.Dynamic].__vnode = vnode.asInstanceOf[js.Any] 170 | } 171 | 172 | /** 173 | * Unlink a VNode and DOM Node. 174 | */ 175 | def unlink(dnode: d.Node): Unit = { 176 | dnode.asInstanceOf[js.Dynamic].__vnode = js.undefined.asInstanceOf[js.Any] 177 | } 178 | 179 | /** 180 | * Get the linked VNode from a DOM Node, if one was linked previously. 181 | */ 182 | def getVNode(dnode: d.Node): Option[VNode] = { 183 | val x = dnode.asInstanceOf[js.Dynamic].__vnode 184 | if (js.DynamicImplicits.truthValue(x)) Some(x.asInstanceOf[VNode]) 185 | None 186 | } 187 | 188 | } 189 | 190 | trait DOMEnvironment extends DOMInstanceMap with DOMUtils 191 | 192 | /** 193 | * Singleton that holds virtual DOM state. Mutable state lives somewhere :-) 194 | */ 195 | object DOMEnvironment extends DOMEnvironment 196 | -------------------------------------------------------------------------------- /js/src/main/scala/im/vdom/backend/dom/DelegateComponent.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package backend 19 | package dom 20 | 21 | import scala.concurrent.ExecutionContext 22 | import scala.concurrent.Future 23 | import scala.scalajs.concurrent.JSExecutionContext.queue 24 | import scala.scalajs.js 25 | import scala.scalajs.js._ 26 | import scala.scalajs.js.UndefOr 27 | import scala.scalajs.js.UndefOr._ 28 | import scala.language._ 29 | 30 | import _root_.org.scalajs.{ dom => d } 31 | 32 | import Defaults._ 33 | 34 | /** 35 | * Manage a `Delegate` associated with a single DOM Node. 36 | * Each DOM node should have at most one Delegate since 37 | * a Delegate manages many event types. 38 | * 39 | * Delegate cleanup is handled by adding a node clean up action. 40 | * 41 | * The Delegate is attached under the __delegate property 42 | * of the node and if a delegate detach action has been added on this node 43 | * already, a property __delegate_cleanupaction property is set to true. 44 | * 45 | */ 46 | trait DelegateComponent { self: ActionLists[d.Node] => 47 | 48 | import events._ 49 | import js.DynamicImplicits.truthValue 50 | 51 | /** 52 | * Set `node.__delegate` to js.undefined. Set Delegate's root to None. 53 | */ 54 | def rmDelegate(dnode: d.Node): d.Node = { 55 | getDelegate(dnode).foreach { d => 56 | d.root(None) // turn everything off 57 | dnode.asInstanceOf[js.Dynamic].__delegate = js.undefined.asInstanceOf[js.Any] 58 | } 59 | dnode 60 | } 61 | 62 | /** 63 | * Link a Delegate to a DOM Node. It replaces any previous 64 | * Delegate linkage that may exist. Adds a cleanup action 65 | * to disconnect the delegate unless one has already been added. 66 | */ 67 | def linkDelegate(del: Delegate, dnode: d.Node): Unit = { 68 | require(dnode != null) 69 | val dyndnode = dnode.asInstanceOf[js.Dynamic] 70 | dyndnode.__delegate = del.asInstanceOf[js.Any] 71 | if (!truthValue(dyndnode.__delegate_cleanupaction)) { 72 | addDetachAction(dnode, cleanupAction(dnode)) 73 | dyndnode.__delegate_cleanupaction = true 74 | } 75 | } 76 | 77 | /** 78 | * Get an optional Delegate linked to a DOM node. 79 | */ 80 | def getDelegate(dnode: d.Node): Option[Delegate] = { 81 | require(dnode != null) 82 | val x = dnode.asInstanceOf[js.Dynamic].__delegate 83 | if (truthValue(x)) Some(x.asInstanceOf[Delegate]) 84 | else None 85 | } 86 | 87 | /** Create a cleanup IOAction that calls rmDelegate and returns the node. */ 88 | private def cleanupAction(node: d.Node) = Action.lift { rmDelegate(node) } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /js/src/main/scala/im/vdom/backend/dom/RenderToDOMComponent.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package backend 19 | package dom 20 | 21 | import scala.concurrent.ExecutionContext 22 | import scala.concurrent.Future 23 | import scala.scalajs.concurrent.JSExecutionContext.queue 24 | import scala.scalajs.js 25 | import scala.scalajs.js._ 26 | import scala.scalajs.js.UndefOr 27 | import scala.scalajs.js.UndefOr._ 28 | import scala.language._ 29 | 30 | import _root_.org.scalajs.{dom => d} 31 | 32 | import Defaults._ 33 | 34 | 35 | /** 36 | * Convert a virtual node to a DOM node. 37 | * 38 | * TODO: Fix the execution context. Ensure that the render output future is 39 | * dependent on the children rendering futures. 40 | */ 41 | trait RenderToDOMComponent extends RendererComponent { 42 | self: Backend with AttributeComponent => 43 | 44 | import DOMEnvironment._ 45 | 46 | type RenderOutput = d.Node 47 | 48 | def render(vnode: VNode)(implicit executor: ExecutionContext): IOAction[RenderOutput] = { 49 | vnode match { 50 | case v@VirtualText(content) => Action.lift { createText(content) } 51 | 52 | case v@VirtualElementNode(tag, attributes, children, key, namespace) => 53 | val makeEl: IOAction[d.Node] = Action.lift { 54 | val newNode = createEl(v) 55 | attributes.foreach(_(newNode)) 56 | newNode 57 | } flatMap { el => 58 | val childrenactions = children.map { child => 59 | val renderChildAction = render(child) 60 | // a bit ugly since appendChild is really a side effoct 61 | renderChildAction.map { c => el.appendChild(c); c } 62 | } 63 | // the child render and append won't happen unless its exposed to be run later 64 | Action.seq(childrenactions: _*) andThen Action.successful(el) 65 | } 66 | makeEl 67 | 68 | case EmptyNode() => 69 | /** Empty nodes become empty divs */ 70 | Action.lift(createEl(VNode.tag("div"))) 71 | 72 | case ThunkNode(f) => render(f()) 73 | 74 | case CommentNode(c) => 75 | Action.lift(d.document.createComment(c)) 76 | } 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /js/src/main/scala/im/vdom/package.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import scala.language._ 20 | 21 | import scalajs.js 22 | import js.UndefOr 23 | import UndefOr._ 24 | import _root_.org.scalajs.{dom => d} 25 | 26 | trait Defaults { 27 | 28 | /** 29 | * Convenience methods for DOM Nodes and Elements. 30 | */ 31 | implicit class RichNode[T <: d.Node](n: T) { 32 | import UndefOr._ 33 | def parentUndefOr: UndefOr[d.Node] = { 34 | val p = n.parentNode 35 | if (p == null) js.undefined 36 | p 37 | } 38 | } 39 | 40 | } 41 | 42 | object Defaults extends Defaults 43 | 44 | /** 45 | * KeyValues that holds a few pieces of function configuration 46 | * information relevant to calling a handler when a dom.Event is fired. 47 | */ 48 | case class FunctionValue(handler: events.Handler, matcher: events.Matcher = events.Matcher.MatchRoot, 49 | useCapture: Option[Boolean] = None) 50 | 51 | /** 52 | * Value that is a function for an event handler. The entire 53 | * event handling capabilities need to be lifted to a string 54 | * based action framework to allow server side rendering to 55 | * work better on jvm. Need to think this through for awhile. 56 | */ 57 | case class FunctionKey(val name: String) extends KeyPart { self => 58 | def ~~>(v: events.Handler) = KeyValue[FunctionValue](self, Some(FunctionValue(v, events.Matcher.MatchRoot, None))) 59 | def ~~>(v: FunctionValue) = KeyValue[FunctionValue](self, Some(v)) 60 | } 61 | 62 | /** 63 | * Convenience class to allow you to define a FunctionKey using '"keyname".func`. 64 | */ 65 | class RichFuncString(val name: String) { 66 | def func = FunctionKey(name) 67 | } -------------------------------------------------------------------------------- /js/src/test/scala/im/events/DelegateSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package events 18 | 19 | import events._ 20 | import org.scalatest.FlatSpec 21 | import org.scalatest.Matchers 22 | import org.scalatest.OptionValues 23 | import _root_.org.scalajs.{dom => d} 24 | 25 | /** 26 | * Test Delegates. 27 | */ 28 | class DelegateSpec extends FlatSpec 29 | with Matchers 30 | with OptionValues { 31 | 32 | import d.document 33 | import vdom.backend.dom.DOMBackend 34 | import scala.scalajs.js.DynamicImplicits.truthValue 35 | 36 | "delegate component" should "attach a Delegate to a node" in { 37 | val p = document.createElement("p") 38 | val d = Delegate(None) 39 | DOMBackend.linkDelegate(d, p) 40 | DOMBackend.getDelegate(p) should not be (null) 41 | } 42 | 43 | it should "allow removing the Delegate" in { 44 | val p = document.createElement("p") 45 | val d = Delegate(None) 46 | DOMBackend.linkDelegate(d, p) 47 | DOMBackend.getDelegate(p) should not be (null) 48 | DOMBackend.rmDelegate(p) 49 | DOMBackend.getDelegate(p) should be(None) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /js/src/test/scala/im/vdom/CleanupSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import _root_.org.scalajs.{ dom => d } 20 | import scalajs.js 21 | 22 | import org.scalatest.FlatSpec 23 | import org.scalatest.Matchers 24 | import org.scalatest.OptionValues 25 | 26 | /** 27 | * Test cleanup actions on DOM nodes and properties. 28 | */ 29 | class CleanupSpec extends FlatSpec 30 | with Matchers 31 | with OptionValues { 32 | 33 | import d.document 34 | import backend.dom.DOMBackend 35 | import js.DynamicImplicits.truthValue 36 | 37 | "cleanup component" should "attach a cleanup queue to a DOM node" in { 38 | val p = document.createElement("p") 39 | DOMBackend.addDetachAction(p, Action.successful(true)) 40 | truthValue(p.asInstanceOf[js.Dynamic].__namedCleanupActions) should be(true) 41 | } 42 | 43 | it should "run the node level cleanup action when asked to cleanup" in { 44 | val p = document.createElement("p") 45 | var counter = 0 46 | DOMBackend.addDetachAction(p, Action.lift { 47 | counter += 1 48 | }) 49 | DOMBackend.runActions(p) 50 | counter should be(1) 51 | } 52 | 53 | it should "allow a named cleanup queue to be created" in { 54 | val p = document.createElement("p") 55 | var counter = 0 56 | DOMBackend.addAction("blah", p, Action.lift { 57 | counter += 1 58 | }) 59 | val t = p.asInstanceOf[js.Dynamic].__namedCleanupActions.asInstanceOf[Map[String, IOAction[_]]] 60 | t.contains("blah") should be(true) 61 | 62 | DOMBackend.runActions(p) 63 | counter should be(1) 64 | } 65 | 66 | it should "process the hierachy of nodes and run all detach actions" in { 67 | val div = document.createElement("div") 68 | var counter = 0 69 | DOMBackend.addDetachAction(div, Action.lift { 70 | counter += 1 71 | }) 72 | val div2 = document.createElement("p") 73 | DOMBackend.addDetachAction(div2, Action.lift { 74 | counter += 1 75 | }) 76 | div.appendChild(div2) 77 | 78 | DOMBackend.runActions(div) 79 | counter should be (2) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /js/src/test/scala/im/vdom/PatchApplicableSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import scala.util._ 20 | import _root_.org.scalajs.{ dom => d } 21 | import concurrent._ 22 | import scalajs.js 23 | import js._ 24 | import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue 25 | 26 | import org.scalatest.AsyncFlatSpec 27 | import org.scalatest.Matchers 28 | import org.scalatest.OptionValues 29 | 30 | /** 31 | * Test rendering virtual nodes to DOM nodes.. 32 | */ 33 | class PatchApplicableSpec extends AsyncFlatSpec 34 | with Matchers 35 | with OptionValues { 36 | 37 | import js.DynamicImplicits.truthValue 38 | import backend.dom.DOMBackend 39 | import DOMBackend._ 40 | import backend.dom.DOMUtils 41 | import VNode._ 42 | 43 | private def cleanBody() = DOMUtils.removeChildren(d.document.body) 44 | 45 | "Applying a patch" should "add an element" in { 46 | val vnode = tag("div") 47 | val body = d.document.body 48 | DOMUtils.removeChildren(body) 49 | body should not equal (null) 50 | val x = DOMBackend.run(InsertPatch(vnode)(body)) 51 | x.map { newNode => 52 | newNode.nodeName shouldEqual "DIV" 53 | body.childNodes.length shouldEqual 1 54 | } 55 | } 56 | 57 | it should "insert a vnode at a specific position" in { 58 | val body = DOMUtils.removeChildren(d.document.body) 59 | val node1 = d.document.createElement("h1") 60 | val node2 = d.document.createElement("h2") 61 | val vnode3 = tag("h3") 62 | val node4 = d.document.createElement("span") 63 | Seq(node1, node2, node4).foreach(body.appendChild(_)) 64 | val x = DOMBackend.run(InsertPatch(vnode3, Some(2))(body)) 65 | x.map { el => 66 | body.childNodes(2).nodeName shouldEqual "H3" 67 | } 68 | } 69 | 70 | it should "add a 2 level div tree automatically" in { 71 | val body = d.document.body 72 | DOMUtils.removeChildren(body) 73 | val vnode = tag("div", tag("div")) 74 | val x = DOMBackend.run(InsertPatch(vnode)(body)) 75 | x.map { el => 76 | body.childNodes.length shouldEqual 1 77 | val div1 = body.childNodes(0) 78 | div1.nodeName should be("DIV") 79 | div1.childNodes.length shouldEqual 1 80 | val div2 = div1.childNodes(0) 81 | div2.nodeName should be("DIV") 82 | div2.childNodes.length shouldEqual 0 83 | } 84 | } 85 | 86 | it should "remove an element" in { 87 | val body = d.document.body 88 | DOMUtils.removeChildren(body) 89 | body.appendChild(d.document.createElement("div")) 90 | body.childNodes.length shouldEqual 1 91 | val x = DOMBackend.run(RemovePatch(body.childNodes(0))) 92 | x.map { el => 93 | el should not be (null) 94 | el.nodeName shouldEqual "DIV" 95 | } 96 | } 97 | 98 | it should "replace an element" in { 99 | val body = DOMUtils.removeChildren(d.document.body) 100 | val vnode = tag("div", tag("div")) 101 | val replacement = tag("p") 102 | val y = for { 103 | newNode <- DOMBackend.run(InsertPatch(vnode)(body)) 104 | replaced <- DOMBackend.run(ReplacePatch(replacement)(newNode)) 105 | } yield replaced 106 | y.map { el => 107 | el.nodeName shouldEqual "P" 108 | body.childNodes.length shouldEqual 1 109 | } 110 | } 111 | 112 | it should "reorder children correctly" in { 113 | val body = DOMUtils.removeChildren(d.document.body) 114 | val children = Seq("p", "h1", "h2", "span").map(d.document.createElement(_)) 115 | val childrenNodeNames = children.map(_.nodeName) 116 | children.foreach(body.appendChild(_)) 117 | (0 until body.childNodes.length).map(body.childNodes(_).nodeName) should contain theSameElementsInOrderAs childrenNodeNames 118 | body.childNodes.length shouldEqual children.length 119 | 120 | // Reorder only. 121 | val instr = ReorderInstruction(moves = Seq((0, 1), (1, 0))) 122 | val x = DOMBackend.run(OrderChildrenPatch(instr)(body)) map { el => 123 | el.childNodes.length shouldEqual children.length 124 | (0 until el.childNodes.length).map(el.childNodes(_).nodeName) should contain theSameElementsInOrderAs Seq("H1", "P", "H2", "SPAN") 125 | } 126 | x 127 | } 128 | 129 | it should "remove and reorder children correctly" in { 130 | val body = DOMUtils.removeChildren(d.document.body) 131 | val children = Seq("p", "h1", "h2", "span").map(d.document.createElement(_)) 132 | val childrenNodeNames = children.map(_.nodeName) 133 | children.foreach(body.appendChild(_)) 134 | 135 | // Reorder and remove. 136 | val instr2 = ReorderInstruction(moves = Seq((2, 1), (1, 2)), removes = Seq(0)) 137 | val y = DOMBackend.run(OrderChildrenPatch(instr2)(body)) map { el => 138 | el.childNodes.length shouldEqual (children.length - 1) 139 | (0 until el.childNodes.length).map(el.childNodes(_).nodeName) should contain theSameElementsInOrderAs Seq("H1", "SPAN", "H2") 140 | } 141 | y 142 | } 143 | 144 | it should "add text from a text patch" in { 145 | val body = cleanBody() 146 | for { 147 | // we run it this way because we don't want to replace BODY, need a DIV 148 | // and then run the TextPatch 149 | x <- DOMBackend.run(InsertPatch(tag("div"))(body)) 150 | y <- DOMBackend.run(TextPatch("my text")(x)) 151 | } yield { 152 | val z: UndefOr[d.Text] = y.asInstanceOf[d.Text] 153 | assert(z.isDefined) 154 | z.get.data shouldEqual "my text" 155 | } 156 | } 157 | 158 | } 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /js/src/test/scala/im/vdom/RenderSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import VNode._ 20 | 21 | import _root_.org.scalajs.{dom => d} 22 | import concurrent._ 23 | import scalajs.js 24 | import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue 25 | 26 | import org.scalatest.AsyncFlatSpec 27 | import org.scalatest.Matchers 28 | import org.scalatest.OptionValues 29 | 30 | /** 31 | * Test rendering virtual nodes to DOM nodes.. 32 | */ 33 | class RenderSpec extends AsyncFlatSpec 34 | with Matchers 35 | with OptionValues { 36 | 37 | import js.DynamicImplicits.truthValue 38 | import backend.dom.DOMBackend 39 | import DOMBackend._ 40 | 41 | "backend rendering" should "create a simple div" in { 42 | val vnode = tag("div") 43 | val elf = DOMBackend.run(render(vnode)) 44 | elf.map { e => 45 | e.nodeName shouldEqual "DIV" 46 | } 47 | } 48 | 49 | it should "render a short, simple tree" in { 50 | val vnode = tag("div", tag("p", text("some text"))) 51 | val elf = DOMBackend.run(render(vnode)) 52 | elf.map { e => 53 | // only the

should count 54 | e.childNodes.length shouldEqual 1 55 | } 56 | } 57 | 58 | it should "create a simple text node" in { 59 | val vnode = text("test") 60 | val elf = DOMBackend.run(render(vnode)) 61 | elf.map { el => 62 | el.textContent shouldEqual "test" 63 | } 64 | } 65 | 66 | it should "allow ThunkNode call its function when its asked to" in { 67 | val vnode = ThunkNode(() => { 68 | tag("div") 69 | }) 70 | val elf = DOMBackend.run(render(vnode)) 71 | elf.map { el => 72 | el.nodeName shouldEqual "DIV" 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /js/src/test/scala/im/vdom/backend/dom/DOMUtilsSpec.scala: -------------------------------------------------------------------------------- 1 | /* Copyright 2015 Devon Miller 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at9 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package im 16 | package vdom 17 | package backend 18 | package dom 19 | 20 | import scala.language._ 21 | import org.scalatest._ 22 | import scala.concurrent.Future 23 | import scala.concurrent.ExecutionContext.Implicits.global 24 | import scala.concurrent.duration._ 25 | import java.util.concurrent.TimeUnit 26 | import java.util.concurrent.atomic.AtomicInteger 27 | import _root_.org.scalajs.dom.{ document => d, Element => El } 28 | 29 | /** 30 | * DOMUtils test. 31 | */ 32 | class DOMUtilsSpec extends FlatSpec with Matchers with OptionValues { 33 | 34 | import Utils._ 35 | import DOMUtils._ 36 | 37 | "canReuseMarkup" should "return true when it should return true" in { 38 | val n = d.createElement("div") 39 | val markup = """

blah is cool
""" 40 | val markupWith = """
blah is cool
""" 41 | n.innerHTML = markupWith 42 | canReuseMarkup(markup, n.firstChild.asInstanceOf[El]) should equal(true) 43 | } 44 | 45 | "removeChildren" should "remove all children" in { 46 | val n = d.createElement("div") 47 | n.innerHTML = """

hello

world

""" 48 | n.childNodes.length should be >(0) 49 | removeChildren(n) 50 | n.childNodes.length should be (0) 51 | 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /jvm/src/main/scala/im/vdom/ScalaXml.scala: -------------------------------------------------------------------------------- 1 | package im.vdom 2 | 3 | import scala.xml.{Text, UnprefixedAttribute, Node} 4 | 5 | object ScalaXml { 6 | def nodeToVNode(xml:Node):VNode = { 7 | val attrs = xml.attributes.collect { case UnprefixedAttribute(k, Text(v), _) => 8 | KeyValue(AttrKey(k), Some(v)) 9 | }.toSeq 10 | val children = xml.nonEmptyChildren 11 | .filter(n => n.label != "#PCDATA" || !n.text.trim.isEmpty) 12 | .map(nodeToVNode) 13 | 14 | if(xml.label == "#PCDATA") VNode.text(xml.text) 15 | else VNode.tag(xml.label, attrs, children:_*) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /jvm/src/test/scala/im/vdom/PatchSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import org.scalatest._ 20 | 21 | /** 22 | * Patch object testing. Patches are just simple domain classes 23 | * so there is not much to test. 24 | */ 25 | class PatchSpec extends FlatSpec 26 | with Matchers 27 | with OptionValues { 28 | 29 | "Patch" should "have only one EmptyPatch" in { 30 | assertResult(EmptyPatch)(EmptyPatch) 31 | } 32 | 33 | it should "not need arguments for RemovePatch" in { 34 | assert(RemovePatch != null) 35 | } 36 | 37 | "andThen" should "sequence left then right" in { 38 | val lhs = EmptyPatch 39 | val rhs = RemovePatch 40 | val andThen = lhs andThen rhs 41 | assertResult(rhs)(andThen.right) 42 | assertResult(lhs)(andThen.left) 43 | } 44 | } -------------------------------------------------------------------------------- /jvm/src/test/scala/im/vdom/ScalaXmlSpec.scala: -------------------------------------------------------------------------------- 1 | package im.vdom 2 | 3 | import org.scalatest.WordSpec 4 | import ScalaXml._ 5 | import VNode._ 6 | 7 | class ScalaXmlSpec extends WordSpec { 8 | "ScalaXml.nodeToVNode()" should { 9 | "convert a trivial empty Node into a VNode.tag" in { 10 | val html =
11 | val expected = tag("br") 12 | 13 | assertResult(expected)(nodeToVNode(html)) 14 | } 15 | 16 | "convert an empty Node with attributes" in { 17 | val html =
18 | val expected = tag("div", Seq(KeyValue(AttrKey("id"), Some("my-id")), KeyValue(AttrKey("class"), Some("my-class")))) 19 | 20 | assertResult(expected)(nodeToVNode(html)) 21 | } 22 | 23 | "convert a Node with text" in { 24 | val html = This is text 25 | val expected = tag("span", text("This is text")) 26 | 27 | assertResult(expected)(nodeToVNode(html)) 28 | } 29 | 30 | "convert a Node with children and attributes" in { 31 | val html = 32 |
33 |
34 | 35 | 36 |
37 |
38 | 39 | val expected = 40 | tag("form", Seq(KeyValue(AttrKey("method"), Some("post"))), 41 | tag("div", 42 | tag("input", Seq(KeyValue(AttrKey("type"), Some("text")), KeyValue(AttrKey("id"), Some("chat-in")), KeyValue(AttrKey("name"), Some("in")))), 43 | tag("input", Seq(KeyValue(AttrKey("type"), Some("submit")), KeyValue(AttrKey("value"), Some("Submit")))) 44 | ) 45 | ) 46 | 47 | assertResult(expected)(nodeToVNode(html)) 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /jvm/src/test/scala/im/vdom/backend/RenderMarkupSpec.scala: -------------------------------------------------------------------------------- 1 | /* Copyright 2015 Devon Miller 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package im 16 | package vdom 17 | package backend 18 | 19 | import scala.concurrent.Await 20 | import scala.concurrent.ExecutionContext.Implicits.global 21 | import scala.concurrent.duration.DurationInt 22 | import scala.language._ 23 | 24 | import org.scalatest.FlatSpec 25 | import org.scalatest.Matchers 26 | import org.scalatest.OptionValues 27 | 28 | class RenderMarkupSpec extends FlatSpec with Matchers 29 | with OptionValues { 30 | 31 | import HTML5Attributes._ 32 | import Styles._ 33 | import VNode.tag 34 | import VNode.text 35 | 36 | "Backend" should "render markup" in { 37 | // val vdom = tag("div", Seq(cls := "highlighted", data("hah") := "blah", heightA := None, 38 | // width := 30, border := "auto", textAlign := "left"), 39 | // tag("p", text("blah")), tag("br")) 40 | // val action = b.render(vdom) 41 | // val output = b.run(action) 42 | // val markup = Await.result(output, 1 seconds) 43 | // println(s"markup: $markup") 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | 5 | val allResolvers = Seq( 6 | Resolver.url("file://" + Path.userHome.absolutePath + "/.ivy/local"), 7 | "sonatype" at "https://oss.sonatype.org/content/repositories/releases") 8 | 9 | //val frontendDeps = Seq("org.scala-js" %%% "scalajs-dom" % "0.8.1") 10 | 11 | val scalatest = "org.scalatest" %% "scalatest" % "3.0.0-M15" 12 | 13 | //val scalaJsDependencies = Seq("org.scala-js" %%% "scalajs-dom" % "0.8.1") 14 | 15 | val testDeps = Seq(scalatest % Test) 16 | 17 | } 18 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.9 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.6") 2 | 3 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.4.0") 4 | 5 | -------------------------------------------------------------------------------- /reactive/index-dev-reactive.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | scalajs-vdom test page 21 | 22 | 35 | 36 | 37 | 38 |

scala-vdom reactive test page

39 | 40 | 41 | 42 | 46 | 47 | 48 |
43 | test1
44 |
45 |
49 | 51 | 53 | 55 | 56 | -------------------------------------------------------------------------------- /reactive/src/main/scala/im/vdom/reactive/Test.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package reactive 19 | 20 | import scala.scalajs.js.JSApp 21 | import scala.concurrent.duration._ 22 | import scala.concurrent.Await 23 | import scala.scalajs.js 24 | import scala.scalajs.js.timers._ 25 | import scala.concurrent.{ Future, ExecutionContext } 26 | import scala.language._ 27 | import scalajs.js 28 | import js.UndefOr 29 | import _root_.org.scalajs.{dom => d} 30 | import d._ 31 | import monifu._ 32 | import monifu.concurrent.Implicits.globalScheduler 33 | import monifu.reactive._ 34 | import channels._ 35 | import subjects._ 36 | import monifu.concurrent._ 37 | import monifu.concurrent.cancelables._ 38 | import monifu.reactive.Ack.Continue 39 | 40 | import events.Handler 41 | import Handler.Implicits._ 42 | import backend.dom.DOMBackend._ 43 | import VNode._ 44 | import HTML5Attributes._ 45 | import Styles._ 46 | import UIEvents._ 47 | import vdom.reactive.Implicits._ 48 | 49 | 50 | /** 51 | * Derived from: https://github.com/staltz/mvi-example. 52 | */ 53 | object Test extends JSApp { 54 | 55 | // Get an element by name. 56 | def elementById[A <: js.Any](id: String): A = 57 | document.getElementById(id).asInstanceOf[A] 58 | 59 | // Convert a d.Element event to an Observable. 60 | def elementEventObservable(el: d.Element, eventType: String): Observable[d.Event] = { 61 | val ps = PublishSubject[d.Event]() 62 | el.addEventListener(eventType, (e: d.Event) => ps.onNext(e)) 63 | ps 64 | } 65 | 66 | // Observe stream of VNodes representing view updates. 67 | // We do not use an auto-render system, but this is the equivalent "rendering system". 68 | def renderer(vtree: Observable[VNode], container: d.Element): BooleanCancelable = { 69 | container.innerHTML = "" 70 | var root: Future[d.Node] = Future(document.createElement("div")) 71 | root.foreach(container.appendChild(_)) 72 | vtree. 73 | startWith(empty). 74 | slidingWindow(2). 75 | subscribe(new monifu.reactive.Observer[Seq[VNode]] { 76 | def onNext(els: Seq[VNode]): Future[Ack] = { 77 | val oldTree = els(0) 78 | val newTree = els(1) 79 | val patch = diff(oldTree, newTree) 80 | root = root.flatMap(el => run(patch(el))) 81 | Ack.Continue 82 | } 83 | def onError(ex: Throwable): Unit = println(s"Error rendering: $ex") 84 | def onComplete() = () 85 | }) 86 | } 87 | 88 | /** 89 | * Convert view click and low-level events to a semantic, user "intent." 90 | */ 91 | def intent(viewInfo: Observable[d.Event]) = viewInfo.map(event => "updateCounter") 92 | 93 | /** 94 | * Convert user "intent" events (like updateCounter) to model state. Expose 95 | * state as an observable. State is kept as 2 fields here for simplicity. 96 | */ 97 | var modelState: Int = 0 98 | val modelList = scala.collection.mutable.ListBuffer[String]() 99 | type ModelType = (Int, Seq[String]) 100 | def model(intentInfo: Observable[String]) = { 101 | intentInfo.map { eventT => 102 | modelList += s"Item - $modelState" 103 | modelState += 1 104 | (modelState, modelList) 105 | } 106 | } 107 | 108 | /** 109 | * Convert model changes to view changes. The VNode observable changes 110 | * can be used by a rendering engine to render into the DOM. The user 111 | * then interacts with that view. 112 | */ 113 | def view(modelInfo: Observable[ModelType]) = { 114 | val clicks = PublishSubject[d.Event]() 115 | val vtreeObs = modelInfo.map { 116 | case (count, list) => 117 | 118 | val listOfThings = list.zipWithIndex.map { 119 | case (item, i) => 120 | tag("li", Some("key" + i.toString), None, Seq(), text(item)) // full form of tag() 121 | } 122 | 123 | tag("div", Some("key1"), None, Seq(), 124 | text(s"count: $count "), 125 | tag("button", Some("buttonkey"), None, Seq(click ~~> Handler{(e: d.Event) => { 126 | clicks.onNext(e) 127 | true 128 | }}), text("Click Me!")), 129 | tag("p", text("Hello World!")), 130 | tag("ul", Some("ulkey"), None, Seq(), 131 | listOfThings: _*)) 132 | } 133 | (vtreeObs, clicks) 134 | } 135 | 136 | // val v1 = tag("div", None, None, Seq()) 137 | // val v2 = tag("div", None, None, Seq()) 138 | // println("v1==v2?: " + (v1 == v2)) 139 | // println("v1 close to v2?: " + (v1 closeTo v2)) 140 | // println("v1 eq v2?: " + (v1 eq v2)) 141 | 142 | def main(): Unit = { 143 | // Dance around circular dependency issues 144 | val placeholder = PublishSubject[ModelType]() 145 | val channel = SubjectChannel(placeholder, OverflowStrategy.Unbounded) 146 | 147 | val (vtree, clicks) = view(placeholder) 148 | val Intent = intent(clicks) 149 | val Model = model(Intent) 150 | renderer(vtree, elementById[d.Element]("test1")) 151 | 152 | // Brake circular dependency and start the "cycle" with the initial state value. 153 | val cancelable = Model.startWith((modelState, Seq())).subscribe(new Observer[ModelType] { 154 | def onNext(p: (Int, Seq[String])) = { 155 | channel.pushNext(p) 156 | Ack.Continue 157 | } 158 | def onError(ex: Throwable): Unit = { 159 | println(s"Error in push: $ex") 160 | channel.pushError(ex) 161 | } 162 | def onComplete() = channel.pushComplete() 163 | }) 164 | } 165 | } -------------------------------------------------------------------------------- /reactive/src/main/scala/im/vdom/reactive/package.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package reactive 19 | 20 | import monifu.reactive._ 21 | 22 | object Implicits { 23 | 24 | import scala.concurrent.duration._ 25 | import scala.concurrent.{ Future, ExecutionContext } 26 | import scala.language._ 27 | 28 | import monifu.reactive._ 29 | import monifu.concurrent._ 30 | import scala.collection.mutable.ArrayBuffer 31 | 32 | implicit class EnhancedObservable[T](source: Observable[T]) { 33 | def slidingWindow(n: Int): Observable[Seq[T]] = source.whileBusyBuffer(OverflowStrategy.Unbounded).buffer(n, 1) 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /shared/src/main/scala/im/vdom/Action.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import scala.concurrent.ExecutionContext 20 | import scala.concurrent.Future 21 | import scala.util.Try 22 | import collection.mutable.ArrayBuffer 23 | import backend.{ Backend, RendererComponent, PatchesComponent } 24 | 25 | /** 26 | * A debuggable object means you can get debuggable information. 27 | */ 28 | trait Debuggable { 29 | def getDebugInfo: DebugInfo 30 | } 31 | 32 | /** 33 | * Debug information is just a string for the moment. 34 | */ 35 | case class DebugInfo(name: String, msg: Option[String] = None, children: Seq[IOAction[_]] = Seq()) 36 | 37 | /** 38 | * Basic action object using the IO monad pattern. The side effect, 39 | * for example changing a DOM element, occurs inside the monad. You 40 | * can represent failure with IOAction so avoid having `R` be another 41 | * wrapper that represents failure. 42 | * 43 | * TODO: Add ExceutionContext as an implicit to handle server side threading. 44 | */ 45 | sealed trait IOAction[+R] extends Debuggable { 46 | def map[R2](f: R => R2)(implicit executor: ExecutionContext): IOAction[R2] = 47 | flatMap[R2](r => SuccessAction[R2](f(r))) 48 | 49 | def flatMap[R2](f: R => IOAction[R2])(implicit executor: ExecutionContext): IOAction[R2] = 50 | FlatMapAction[R2, R](this, f, executor) 51 | 52 | /** 53 | * Sequence this before next. 54 | */ 55 | def andThen[R2](next: IOAction[R2]): IOAction[R2] = AndThenAction[R2](this, next) 56 | 57 | /** 58 | * Filter the result with p 59 | */ 60 | final def filter(p: R => Boolean)(implicit executor: ExecutionContext): IOAction[R] = withFilter(p) 61 | 62 | /** 63 | * Helper for for-comprehensions. 64 | */ 65 | def withFilter(p: R => Boolean)(implicit executor: ExecutionContext): IOAction[R] = 66 | flatMap(v => if (p(v)) SuccessAction(v) else throw new NoSuchElementException("IOAction.withFilter failed")) 67 | 68 | /** 69 | * Run another action after this one that processes a potential failure. The cleanup 70 | * action is computed from the result of this action. The cleanup argument is None 71 | * if this action succeeds or a Some containing this action's failure if it failed. 72 | * The default `keepFailure=true` says that this actions exception should be kept even if the cleanup's 73 | * action throws an exception. If false, the exception from the cleanup action is kept. 74 | * @param keepFailure if the cleanup action fails, keep that failure info if false. Otherwise, keep the original failure if true. 75 | */ 76 | def cleanUp(f: Option[Throwable] => IOAction[_], keepFailure: Boolean = true)(implicit executor: ExecutionContext): IOAction[R] = 77 | CleanUpAction[R](this, f, keepFailure, executor) 78 | 79 | /** 80 | * Run an action after this action, regardless if this action succeeds or fails. If this 81 | * action fails, always propagate that failure. If this action succeeds and 82 | * the second action fails, return the second action's failure. This is much like 83 | * a 'try { ... } finally { ... }` clause. 84 | */ 85 | def andFinally(a: IOAction[_]): IOAction[R] = cleanUp(_ => a)(Action.sameThreadExecutionContext) 86 | 87 | /** 88 | * Return an action that contains the throwable that this action failed with as its result. 89 | * If this action succeeded, the resulting action fails with a NoSuchElementException. 90 | * This is a projection of the action. 91 | */ 92 | def failed: IOAction[Throwable] = FailedAction(this) 93 | 94 | /** 95 | * Convert to a Try. Use cleanup and andFinally before using asTry. 96 | */ 97 | def asTry: IOAction[Try[R]] = AsTryAction[R](this) 98 | 99 | /** 100 | * Run another action after this action, if it completed successfully, and return the result 101 | * of both actions. If either of the two actions fails, the resulting action also fails. 102 | */ 103 | def zip[R2](a: IOAction[R2]): IOAction[(R, R2)] = 104 | SequenceAction[Any, ArrayBuffer[Any]](Vector(this, a)).map { r => 105 | (r(0).asInstanceOf[R], r(1).asInstanceOf[R2]) 106 | }(Action.sameThreadExecutionContext) 107 | 108 | } 109 | 110 | /** 111 | * Helper functions. 112 | */ 113 | object Action { 114 | 115 | /** Convert a `Future` to a [[IOAction]]. */ 116 | def from[R](f: Future[R]): IOAction[R] = FutureAction[R](f) 117 | 118 | /** Lift a constant value to a [[IOAction]]. This immediately evaluates the argument. Use lift to delay evaluation. */ 119 | def successful[R](v: R): IOAction[R] = SuccessAction[R](v) 120 | 121 | /** Create a [[IOAction]] that always fails. */ 122 | def failed(t: Throwable): IOAction[Nothing] = FailureAction(t) 123 | 124 | /** Compose actions from the varargs actions, run in sequence using `andThen`, and return Unit. */ 125 | def seq(actions: IOAction[_]*): IOAction[Unit] = 126 | (actions :+ SuccessAction(())).reduceLeft(_ andThen _).asInstanceOf[IOAction[Unit]] 127 | 128 | /** 129 | * Create a IOAction that runs some other actions in sequence and combines their results 130 | * with the given function. 131 | */ 132 | def fold[T](actions: Seq[IOAction[T]], zero: T)(f: (T, T) => T)(implicit ec: ExecutionContext): IOAction[T] = 133 | actions.foldLeft[IOAction[T]](Action.successful(zero)) { (za, va) => za.flatMap(z => va.map(v => f(z, v))) } 134 | 135 | /** 136 | * An ExecutionContext used internally for executing plumbing operations during IOAction 137 | * composition. 138 | */ 139 | private[vdom] object sameThreadExecutionContext extends ExecutionContext { 140 | override def execute(runnable: Runnable): Unit = runnable.run() 141 | override def reportFailure(t: Throwable): Unit = throw t 142 | } 143 | 144 | /** 145 | * Lift a function that takes a context. 146 | */ 147 | def withContext[R, B <: Backend](f: B#Context => R) = ContextualAction(f) 148 | 149 | /** 150 | * Lift a by-name value into an IOAction. This delays the computation until it is needed. 151 | */ 152 | def lift[R, B <: Backend](f: => R) = ContextualAction(f) 153 | } 154 | 155 | /** 156 | * Context used when running an action. 157 | */ 158 | trait ActionContext 159 | 160 | /** 161 | * When applied, performs the patch action. Return the result 162 | * of the action. This action has access to the Backend's 163 | * context while executing. 164 | * 165 | * @tparam R output after applying patch 166 | * @tparam B the `Backend` 167 | * 168 | */ 169 | trait ContextualAction[+R, -B <: Backend] extends IOAction[R] { self => 170 | /** 171 | * Run this action with a context. 172 | */ 173 | def run(ctx: B#Context): R 174 | } 175 | 176 | /** 177 | * Create some convenience functions to create `IOAction`s. Generally, 178 | * a layer in between the programmer and the Backend hides the 179 | * creation of actions. 180 | */ 181 | object ContextualAction { 182 | /** 183 | * Create a patch action 184 | */ 185 | def apply[R, B <: Backend](f: B#Context => R) = new ContextualAction[R, B] { 186 | def run(ctx: B#Context) = f(ctx) 187 | def getDebugInfo = DebugInfo("contextual patch") 188 | } 189 | 190 | /** 191 | * Create a patch action from a simple action. It ignores the context. 192 | */ 193 | def apply[R, B <: Backend](f: => R) = new ContextualAction[R, B] { 194 | def run(ctx: B#Context) = f 195 | def getDebugInfo = DebugInfo("contextual patch") 196 | } 197 | } 198 | 199 | /** 200 | * Perform a flatMap using f. 201 | */ 202 | case class FlatMapAction[+R, P](base: IOAction[P], f: P => IOAction[R], executor: ExecutionContext) extends IOAction[R] { 203 | def getDebugInfo = DebugInfo("flatMap", children = Seq(base)) 204 | } 205 | 206 | /** 207 | * Performs left then right 208 | */ 209 | case class AndThenAction[+R](left: IOAction[_], right: IOAction[R]) extends IOAction[R] { 210 | def getDebugInfo = DebugInfo("andThen", children = Seq(left, right)) 211 | } 212 | 213 | /** 214 | * Clean up with f after a failure. 215 | */ 216 | case class CleanUpAction[+R](base: IOAction[R], f: Option[Throwable] => IOAction[_], 217 | keepFailure: Boolean, executor: ExecutionContext) extends IOAction[R] { 218 | def getDebugInfo = DebugInfo("cleanUp", children = Seq(base)) 219 | } 220 | 221 | /** 222 | * An action that returns a constant value. 223 | */ 224 | case class SuccessAction[+R](value: R) extends ContextualAction[R, Backend] { 225 | def run(ctx: Backend#Context): R = value 226 | def getDebugInfo = DebugInfo("success", Some(value.toString)) 227 | } 228 | 229 | /** 230 | * An action that fails with the given throwable when run. 231 | */ 232 | case class FailureAction(t: Throwable) extends ContextualAction[Nothing, Backend] { 233 | def run(ctx: Backend#Context): Nothing = throw t 234 | def getDebugInfo = DebugInfo("failure", Some(t.toString)) 235 | } 236 | 237 | /** 238 | * An asynchronous DBIOAction that returns the result of a Future. 239 | */ 240 | case class FutureAction[+R](f: Future[R]) extends IOAction[R] { 241 | def getDebugInfo = DebugInfo("future ", Some(f.toString)) 242 | } 243 | 244 | /** 245 | * Represents a failed action 246 | */ 247 | case class FailedAction(a: IOAction[_]) extends IOAction[Throwable] { 248 | def getDebugInfo = DebugInfo("failed", children = Seq(a)) 249 | } 250 | 251 | /** 252 | * Represents conversion to a Try 253 | */ 254 | case class AsTryAction[+R](a: IOAction[R]) extends IOAction[Try[R]] { 255 | def getDebugInfo = DebugInfo("asTry", children = Seq(a)) 256 | } 257 | 258 | import collection.generic._ 259 | 260 | /** A DBIOAction that represents a `sequence` operation for sequencing in the DBIOAction monad. */ 261 | case class SequenceAction[R, +R2](as: IndexedSeq[IOAction[R]])(implicit val cbf: CanBuild[R, R2]) extends IOAction[R2] { 262 | def getDebugInfo = DebugInfo("sequence", children = as) 263 | } 264 | -------------------------------------------------------------------------------- /shared/src/main/scala/im/vdom/Attributes.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import scala.language._ 20 | 21 | /** 22 | * Various constants related to DOM. 23 | */ 24 | trait Constants { 25 | 26 | /** 27 | * Namespace URIs 28 | */ 29 | object NS { 30 | val HTML5 = "http://www.w3.org/1999/xhtml" 31 | val SVG = "http://www.w3.org/2000/svg" 32 | val XLINK = "http://www.w3.org/1999/xlink" 33 | val XML = "http://www.w3.org/XML/1998/namespace" 34 | } 35 | } 36 | 37 | object Constants extends Constants 38 | 39 | trait HTML5Attributes { 40 | 41 | implicit class StandardStringToKey(name: String) extends RichString(name) 42 | 43 | val accept = "accept".attr 44 | val acceptCharSet = "accept-charset".attr 45 | val accessKey = "accessKey".attr 46 | val action = "action".attr 47 | val allowFullScreen = "allowFullScreen".attr 48 | val allowTransparency = "allowTransparency".attr 49 | val alt = "alt".attr 50 | val checked = "checked".attr 51 | val `class` = "class".attr 52 | val cls = `class` 53 | var contentEditable = "contenteditable".attr 54 | val contextMenu = "contextmenu".attr 55 | val disabled = "disabled".attr 56 | val dir = "dir".attr 57 | val hidden = "hidden".attr 58 | val href = "href".attr 59 | val httpEquiv = "http-equiv".attr 60 | val htmlFor = "for".attr 61 | val innerHTML = "innerHTML".attr 62 | val id = "id".attr 63 | val lang = "lang".attr 64 | val scope = "scope".attr 65 | val scoped = "scoped".attr 66 | val scrolling = "scrolling".attr 67 | val seamless = "seamless".attr 68 | val selected = "selected".attr 69 | val shape = "shape".attr 70 | val size = "size".attr 71 | val sizes = "sizes".attr 72 | val span = "span".attr 73 | val spellCheck = "spellCheck".attr 74 | val src = "src".attr 75 | val srcDoc = "srcDoc".attr 76 | val style = "style".attr 77 | val tabIndex = "tabindex".attr 78 | val title = "title".attr 79 | val value = "value".attr 80 | val widthA = "width".attr // conflicts with style 81 | val heightA = "height".attr // conflicts with style 82 | 83 | // Don't forget these... 84 | /** 85 | * async autoComplete autoFocus autoPlay 86 | * 87 | * cellPadding cellSpacing charSet checked 88 | * classID className colSpan cols content contentEditable contextMenu controls 89 | * coords crossOrigin 90 | * data dateTime defer dir disabled download draggable 91 | * encType 92 | * form formAction formEncType formMethod formNoValidate formTarget frameBorder 93 | * 94 | * headers height hidden high href hrefLang htmlFor httpEquiv 95 | * icon id 96 | * label lang list loop low 97 | * manifest marginHeight marginWidth max maxLength media mediaGroup 98 | * method min multiple muted 99 | * 100 | * name noValidate 101 | * open optimum 102 | * pattern placeholder poster preload 103 | * radioGroup readOnly rel required role rowSpan rows sandbox 104 | * 105 | * srcSet start step style 106 | * tabIndex target title type 107 | * useMap 108 | * value 109 | * width wmode 110 | */ 111 | } 112 | 113 | trait CustomHTML5Attributes { 114 | implicit class CustomStringToKey(name: String) extends RichString(name) 115 | def data(name: String) = AttrKey("data-" + name) 116 | def aria(name: String) = AttrKey("aria-" + name) 117 | } 118 | 119 | object CustomHTML5Attributes extends CustomHTML5Attributes 120 | 121 | /** 122 | * Standard HTML5 attributes. Usually, you'll import this. 123 | */ 124 | object HTML5Attributes extends HTML5Attributes with CustomHTML5Attributes 125 | 126 | /** Attributes for SVG. */ 127 | trait SVGAttributes { 128 | import Constants.NS._ 129 | implicit class StandardStringToKey(name: String) extends RichString(name) 130 | 131 | val clipPath = AttrKey(name = "clip-art") 132 | val cx = "cx".attr 133 | val cy = "cx".attr 134 | val d = "d".attr 135 | val dx = "dx".attr 136 | val dy = "dy".attr 137 | val fill = "fill".attr 138 | val fillOpacity = "fill-opacity".attr 139 | val fontFamily = "font-family".attr 140 | val fontSize = "font-size".attr 141 | val fx = "fx".attr 142 | val fy = "fy".attr 143 | val gradientTransform = "gradientTransform".attr 144 | val gradientUnits = "gradientUnits".attr 145 | val markerEnd = "marker-end".attr 146 | val markerMid = "marker-mid".attr 147 | val markerStart = "marker-start".attr 148 | val offset = "offset".attr 149 | val opacity = "opacity".attr 150 | val patternContentUnits = "patternContentUnits".attr 151 | val patternUnits = "patternUnits".attr 152 | val points = "points".attr 153 | val preserveAspectRatio = "preserveAspectRatio".attr 154 | val r = "r".attr 155 | val rx = "rx".attr 156 | val ry = "ry".attr 157 | val spreadMethod = "spreadMethod".attr 158 | val stopColor = "stop-color".attr 159 | val stopOpacity = "stroke-opacity".attr 160 | val stroke = "stroke".attr 161 | val strokeDasharray = "stroke-dasharray".attr 162 | val strokeLinecap = "stoke-linecap".attr 163 | val strokeOpacity = "stroke-opacity".attr 164 | val strokeWidth = "stroke-width".attr 165 | val textAnchor = "text-anchor".attr 166 | val transform = "transform".attr 167 | val version = "version".attr 168 | val viewBox = "view-box".attr 169 | val x1 = "x1".attr 170 | val x2 = "x2".attr 171 | val x = "x".attr 172 | val xlinkActuate = AttrKey("xlink:actuate", Some(XLINK)) 173 | val xlinkArcrole = AttrKey("xlink:arcrole", Some(XLINK)) 174 | val xlinkHref = AttrKey("xlink:href", Some(XLINK)) 175 | val xlinkRole = AttrKey("xlink:role", Some(XLINK)) 176 | val xlinkShow = AttrKey("xlink:show", Some(XLINK)) 177 | val xlinkTitle = AttrKey("xlink:title", Some(XLINK)) 178 | val xlinkType = AttrKey("xlink:type", Some(XLINK)) 179 | val xmlBase = AttrKey("xml:base", Some(XML)) 180 | val xmlLang = AttrKey("xml:lang", Some(XML)) 181 | val xmlSpace = AttrKey("xml:space", Some(XML)) 182 | val y1 = "y1".attr 183 | val y2 = "y2".attr 184 | val y = "y".attr 185 | } 186 | 187 | object SVGAttributes extends SVGAttributes 188 | 189 | /** 190 | * Style attributes. These attributes are set on the style property 191 | * of an element. Some of them duplicate attributes found on other 192 | * elements e.g. height and width. 193 | */ 194 | trait Styles { 195 | implicit class StyleToKey(name: String) extends RichString(name) 196 | val border = "border".style 197 | val fill = "fill".style 198 | val height = "height".style 199 | val heightS = height 200 | val lineHeight = "lineHeight".style 201 | val opacity = "opacity".style 202 | val order = "order".style 203 | val stroke = "stroke".style 204 | val tabSize = "tabSize".style 205 | val textAlign = "textAlign".style 206 | val width = "width".style 207 | val widthS = width 208 | val zIndex = "zIndex".style 209 | } 210 | 211 | object Styles extends Styles 212 | -------------------------------------------------------------------------------- /shared/src/main/scala/im/vdom/Diff.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | /** 20 | * It's not clear why I can't just build this into the VNode trait 21 | * and split this out to subclasses versus having this 22 | * function stuck out here in the middle of nowhere. 23 | */ 24 | private[vdom] trait Diff { 25 | 26 | import VNodeUtils._ 27 | 28 | /** 29 | * Diff two VNode's and return a patch that makes original look like target. 30 | * This function determines if the original and target are so different 31 | * that it has to be replaced by a new node (e.g. insert, remove or replace) 32 | * versus an update. If they are similar, they are diffed against 33 | * each other. 34 | * 35 | * If the original vnode is empty, an InserttPatch is created. 36 | * 37 | * This is the primary function to call to diff two unknown virtual nodes. 38 | * 39 | * @param original the current VNode description 40 | * @param target the target VNode description 41 | * @return a Patch that would make original look like target 42 | */ 43 | def diff(original: VNode, target: VNode): Patch = { 44 | doDiff(original, target, Nil) 45 | } 46 | 47 | def doDiff(original: VNode, target: VNode, path: Seq[Int] = Nil): Patch = { 48 | if (original == target) EmptyPatch 49 | else if (original == VNode.empty) ReplacePatch(target).applyTo(path) 50 | else if (target == VNode.empty) RemovePatch.applyTo(path) 51 | else (original, target) match { 52 | case (o: VirtualText, t: VirtualText) => o.diff(t, path) 53 | case (o: VirtualElementNode, t: VirtualElementNode) => o.diff(t, path) 54 | case (o: ThunkNode, t: ThunkNode) => o.diff(t, path) 55 | case (o: ThunkNode, _) => doDiff(o.f(), target, path) 56 | case (_, t) => ReplacePatch(target).applyTo(path) 57 | } 58 | } 59 | 60 | /** 61 | * Totally unoptimized :-). We should sort and see if any 62 | * attributes are the same, leave those in place and only 63 | * patch to remove attributes. 64 | */ 65 | def diffProperties(original: Seq[KeyValue[_]], target: Seq[KeyValue[_]]): Patch = { 66 | val deletes = original.diff(target).map { x => 67 | KeyValuePatch(Seq(x.unset)) 68 | } 69 | val adds = target.diff(original).map { x => 70 | KeyValuePatch(Seq(x)) 71 | } 72 | deletes ++ adds 73 | } 74 | 75 | /** 76 | * Returns moves (Seq[(Int,Int)], removed Seq[Int], added Seq[Int], restdiff Seq[Patch]). 77 | * The moves should operate after the removes on the original and adds on the 78 | * target have been factored out. 79 | * 80 | * TODO Redo the entire algorithm. 81 | */ 82 | def diffSeq2(original: Seq[VNode], target: Seq[VNode], path: Seq[Int]): (Seq[(Int, Int)], Seq[Int], Seq[Int], Seq[Patch]) = { 83 | // Find removes 84 | println("original:") 85 | original.zipWithIndex.foreach { case (a, i) => println(s"$i: $a") } 86 | println("target:") 87 | target.zipWithIndex.foreach { case (a, i) => println(s"$i: $a") } 88 | 89 | // Find nodes that were removed. Indexes valid against original sequence. 90 | // Seq(index,...) 91 | val removedIndexes = findRemoves(original, target)(_ == _) 92 | 93 | // Create tmp original with removes removed 94 | // Seq((vnode, orig index in original), ...) 95 | val origLessRemoves = 96 | original.zipWithIndex.filterNot { case (node, index) => removedIndexes.contains(index) } 97 | 98 | // Find nodes that are pure adds. Indexes are relative to target sequence. 99 | // Return Seq(index),...) 100 | val addedIndexes = findAdds(original, target)(_ == _) 101 | 102 | // Create tmp target sequence with "adds" removed. 103 | // Seq((vnode, orig index in target), ...) 104 | val targetLessAdded = 105 | target.zipWithIndex.filterNot { case (node, index) => addedIndexes.contains(index) } 106 | 107 | println("origLessRemoves:") 108 | origLessRemoves.zipWithIndex.foreach { case (a, i) => println(s"$i: $a") } 109 | println("targetLessAdded:") 110 | targetLessAdded.zipWithIndex.foreach { case (a, i) => println(s"$i: $a") } 111 | 112 | // Find moved items taking into account keys where available. 113 | // Operate on original with removes removed and target with adds removed. 114 | // Remove moves that are to and from the same position since that's not really a move. 115 | // Seq((original index, target index), ...) 116 | val moves = origLessRemoves.map { orig => 117 | targetLessAdded.indexWhere { tgt => orig._1 == tgt._1 } 118 | }.zipWithIndex.filter(_._1 >= 0).filterNot(p => p._1 == p._2) 119 | 120 | println(s"moves (${moves.length}): $moves") 121 | println(s"removed (${removedIndexes.length}): $removedIndexes") 122 | println(s"adds (${addedIndexes.length}): $addedIndexes") 123 | println(s"targetLessAdded ($targetLessAdded.length}):") 124 | targetLessAdded.zipWithIndex.foreach { case (a, i) => println(s"$i: $a") } 125 | 126 | // 127 | // For the reduced original sequence, it should patch index-wise to the reduce target 128 | // sequence. Now we can run a pure diff on those to find any diffs that reflect 129 | // "close" node definitions. Those that are the same should work out to an empty diff 130 | // automatically. 131 | // 132 | val origLessRemovesWithMoves = collection.mutable.IndexedSeq(origLessRemoves: _*) 133 | moves.foreach { 134 | case (from, to) => 135 | origLessRemovesWithMoves(to) = origLessRemoves(from) 136 | } 137 | println(s"origLessRemovesWithMoves:") 138 | origLessRemovesWithMoves.zipWithIndex.foreach { case (a, i) => println(s"$i: $a") } 139 | 140 | assert(origLessRemovesWithMoves.size == targetLessAdded.size) 141 | 142 | val restdiff: Seq[Patch] = origLessRemovesWithMoves.zip(targetLessAdded).zipWithIndex.map { 143 | case ((l, r), index) => 144 | doDiff(l._1, r._1, path :+ index) 145 | } 146 | println(s"child diffs: $restdiff") 147 | 148 | (moves, removedIndexes, addedIndexes, restdiff) 149 | } 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | /** 158 | * Observes keyed VNodes and tries to identify moved nodes. 159 | * The algorithm has almost no other optimizations :-). 160 | * 161 | * Optimizations it does have,rReordering children. 162 | * 163 | * It does not track keyed objects that are on separate levels. 164 | */ 165 | def diffSeq(original: Seq[VNode], target: Seq[VNode], path: Seq[Int]): Patch = { 166 | val tsize = target.size 167 | if (target.size == 0) { 168 | // Remove all the original nodes. 169 | original.zipWithIndex.map { 170 | case (value, index) => doDiff(value, VNode.empty, path :+ index) 171 | } 172 | } else { 173 | val (moves, removedIndexes, addedIndexes, restdiff) = diffSeq2(original, target, path) 174 | (OrderChildrenPatch(ReorderInstruction(moves, removedIndexes)) andThen 175 | restdiff andThen 176 | addedIndexes.map { index => InsertPatch(target(index), Some(index)) }).applyTo(path) 177 | } 178 | } 179 | 180 | } 181 | 182 | private[vdom] object Diff extends Diff -------------------------------------------------------------------------------- /shared/src/main/scala/im/vdom/Keys.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import scala.language._ 20 | 21 | /** 22 | * Keys are really builders of `KeyValue` objects. 23 | */ 24 | trait KeyPart { self => 25 | def name: String 26 | def namespace: Option[String] = None 27 | } 28 | 29 | /** 30 | * Attribute that is part of an element. 31 | */ 32 | case class AttrKey(val name: String, override val namespace: Option[String] = None) extends KeyPart { self => 33 | def :=[T](v: Option[T]): KeyValue[T] = KeyValue[T](this, v) 34 | def :=[T](v: T): KeyValue[T] = :=[T](Some(v)) 35 | } 36 | 37 | /** 38 | * Property that is part of a style. 39 | */ 40 | case class StyleKey(val name: String) extends KeyPart { self => 41 | def :=[T](v: Option[T]): KeyValue[T] = KeyValue[T](this, v) 42 | def :=[T](v: T): KeyValue[T] = :=[T](Some(v)) 43 | } 44 | 45 | /** 46 | * Combination of keys and values. A value of None should indicate 47 | * that something should be unset or removed. What unset or remove 48 | * means is Backend dependent. 49 | * 50 | */ 51 | case class KeyValue[T](val key: KeyPart, val value: Option[T]) { 52 | /** 53 | * Convenience function to create an unset KeyValue. 54 | */ 55 | def unset = copy(value = None) 56 | } 57 | 58 | /** 59 | * Create KeyParts easily given a string: `"blahkey".attr` creates 60 | * an `AttrKey`. 61 | */ 62 | class RichString(val name: String) { 63 | def attr = AttrKey(name) 64 | def style = StyleKey(name) 65 | } 66 | 67 | /** 68 | * The exception type in this system. 69 | */ 70 | class VDomException(msg: String, parent: Throwable = null) extends RuntimeException(msg, parent) 71 | 72 | /** 73 | * Add a queue to an object for named IOAction lists. The actions can be 74 | * run by calling `runActions`. Running actions should be considered a side effect. 75 | * Once added, an IOAction cannot be removed from the list. The API clearly 76 | * shows that the object implementing this trait is mutable. 77 | * 78 | */ 79 | trait ActionLists[T] { 80 | /** 81 | * Add a cleanup action to run just before an attribute's value is set 82 | * to a new value. The specified actions are run after already registered actions. 83 | */ 84 | def addAction(keyName: String, node: T, action: IOAction[_]*): Unit 85 | /** 86 | * Add an action to run after the Node is detached from its document. 87 | * The specified actions are run after the already registered actions. 88 | * Detaching should invoke hierarchical action running if T contains a hierarchy. 89 | * Whether children action lists are run prior to the top nodes action list 90 | * is implementation dependent. 91 | */ 92 | def addDetachAction(node: T, action: IOAction[_]*): Unit 93 | /** 94 | * Run the detach actions for el and named. The actions are run automatically by the PatchesComponent 95 | * at the right time of the lifecycle. Reset all action lists. The named lists are run first 96 | * then the node level queue. Cleanup should recurse through the children nodes as well. 97 | */ 98 | def runActions(node: T): Unit 99 | 100 | /** Run the cleanup actions for the named queue. */ 101 | def runActions(name: String, node: T): Unit 102 | } 103 | -------------------------------------------------------------------------------- /shared/src/main/scala/im/vdom/Patch.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import scala.concurrent.{ Promise, ExecutionContext, Future } 20 | import scala.util.{ Try, Success, Failure } 21 | import scala.util.control.NonFatal 22 | 23 | /** 24 | * A Patch holds "difference" information that can be applied 25 | * to another object (the target) to make it look like another 26 | * (the source) object. The "difference" information must 27 | * be interpreted for each backend environment. 28 | * 29 | * You can create patches either from diffing two virtual DOM 30 | * trees or you can just manually create the patches and compose 31 | * them using a sequence or `andThen` essentially creating a 32 | * patch stream or template language of DOM "updates." 33 | * 34 | * You can create your own DSL to create patches if you want. 35 | */ 36 | sealed trait Patch { 37 | /** 38 | * Route this path to another part of the sub-tree when its run. 39 | * The first sequence position represents the first level 40 | * of children that the patch is applied to and so on. 41 | * Nil means to act on the node that the patch is applied to. 42 | */ 43 | def applyTo(path: Seq[Int] = Nil) = PathPatch(this, path) 44 | 45 | /** 46 | * Route this path to a specific child index when it is run. 47 | * The index is zero-based. An index outside the child 48 | * list range results in an error. 49 | */ 50 | def applyTo(path: Int) = PathPatch(this, Seq(path)) 51 | 52 | /** 53 | * Sequence a patch before another. 54 | */ 55 | def andThen(right: Patch) = AndThenPatch(this, right) 56 | } 57 | 58 | /** 59 | * Do not do anything. This may translate into some type of "dummy" element. 60 | */ 61 | case object EmptyPatch extends Patch 62 | 63 | /** 64 | * Apply a patch to a particular tree child. Indexes navigate children. 65 | */ 66 | case class PathPatch(patch: Patch, path: Seq[Int] = Nil) extends Patch 67 | 68 | /** 69 | * Remove a node. 70 | */ 71 | case object RemovePatch extends Patch 72 | 73 | /** 74 | * Replace a node. Access to the parent will be required. 75 | */ 76 | case class ReplacePatch(replacement: VNode) extends Patch 77 | 78 | /** Insert a new child at the specified index, or append if index is not specified. */ 79 | case class InsertPatch(vnode: VNode, index: Option[Int] = None) extends Patch 80 | 81 | /** Create a text node. */ 82 | case class TextPatch(content: String) extends Patch 83 | 84 | /** 85 | * Apply attributes/properties to an element. 86 | */ 87 | case class KeyValuePatch(elActions: Seq[KeyValue[_]]) extends Patch 88 | 89 | /** Manipulate children. */ 90 | case class OrderChildrenPatch(i: ReorderInstruction) extends Patch 91 | 92 | /** 93 | * Combine two patches in sequence. 94 | */ 95 | case class AndThenPatch(left: Patch, right: Patch) extends Patch 96 | 97 | /** 98 | * Instruction to re-order children. Removes should be processed first 99 | * then the moves. Duplicate indexes in any of these structures could 100 | * produce surprises. Moves should reflect the removes that are 101 | * processed first. 102 | */ 103 | case class ReorderInstruction(moves: Seq[(Int, Int)] = Seq(), removes: Seq[Int] = Seq()) 104 | -------------------------------------------------------------------------------- /shared/src/main/scala/im/vdom/VNode.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import collection.mutable 20 | 21 | import Diff._ 22 | 23 | /** 24 | * An object that can be diff'd to produce a patch that takes this object into that. 25 | * 26 | * We pull the type parameter into an abstract type so we do not have to repeat it 27 | * everywhere which qucikly becomes inconvienent. You only diff with objects 28 | * with the same class as yourself. This uses F-bounded polymorphism so that the diff 29 | * function only works on the same object type it is declared in. You can only 30 | * diff on the same type as this. 31 | */ 32 | trait Diffable { self => 33 | type That >: self.type <: Diffable 34 | /** 35 | * Diff this object with another. The other object needs to be the same class. 36 | * The patch returned should reflect `path` and any additional pathing needed. 37 | */ 38 | def diff(that: That, path: Seq[Int]): Patch 39 | } 40 | 41 | /** A type that can provide a key. */ 42 | trait Keyable { 43 | /** Default key is None */ 44 | def key: Option[VNodeKey] = None 45 | } 46 | 47 | /** 48 | * Virtual node can be diffed and keyed. Some virtual dom libraries 49 | * like to add hooks (hook and unhook) to be called when the vnode 50 | * is associated with an actual DOM node. In this library, there are 51 | * no hooks but you can easily compose actions to map into a patch 52 | * action and add your hook and unhook logic through composition. 53 | * 54 | */ 55 | trait VNode extends Keyable with Diffable { 56 | def closeToOrEquals(rhs: VNode): Boolean = VNodeUtils.closeToOrEquals(this, rhs) 57 | } 58 | 59 | /** 60 | * Node composed of text content. 61 | */ 62 | case class VirtualText(text: String) extends VNode { 63 | type That = VirtualText 64 | 65 | def diff(that: That, path: Seq[Int] = Nil): Patch = { 66 | val r = 67 | if (text == that.text) EmptyPatch 68 | else TextPatch(that.text) 69 | if (path != Nil) r.applyTo(path) 70 | r 71 | } 72 | } 73 | 74 | object VNodeUtils { 75 | /** 76 | * Find removed els. Return indexes of removes. Indexes may not be sorted. 77 | * Uses `closToOrEquals` to use fuzzy node comparison. 78 | */ 79 | def findRemoves[T](source: Seq[T], target: Seq[T])(compare: (T, T) => Boolean): Seq[Int] = 80 | // add index to each item, create (index, deleted) flags, filter to keep deletes, return only indexes 81 | source.zipWithIndex.map { 82 | case (vnode, index) => 83 | (index, kindOfContains(target, vnode)(compare)) 84 | }.filterNot(_._2).map(_._1) 85 | 86 | def findRemovesCloseEnough[T] = findRemoves[T](_: Seq[T], _: Seq[T])(closeToOrEquals) 87 | 88 | /** 89 | * Find added els. Return indexes of adds relative to the target sequence. Indexes may not be sorted. 90 | */ 91 | def findAdds[T](source: Seq[T], target: Seq[T])(compare: (T, T) => Boolean = closeToOrEquals _): Seq[Int] = 92 | findRemoves(target, source)(compare) 93 | 94 | def findAddsCloseEnough[T] = findAdds[T](_: Seq[T], _: Seq[T])(closeToOrEquals) 95 | 96 | /** 97 | * Like "Sequence.contains` but uses the specified comparison for the test. 98 | */ 99 | def kindOfContains[T](seq: Seq[T], vnode: T)(compare: (T, T) => Boolean): Boolean = 100 | seq.find { compare(_, vnode) }.fold(false)(_ => true) 101 | 102 | /** 103 | * Like `Sequence.contains` but uses `closeToOrEqual` for the test. 104 | */ 105 | def kindOfContainsCloseEnough[T] = kindOfContains[T](_: Seq[T], _: T)(closeToOrEquals) 106 | 107 | /** 108 | * Determine if two nodes are equal using standard equals, or for VirtualElementNodes, close to each other 109 | * using `VirtualElementNode.closeTo`. 110 | */ 111 | def closeToOrEquals[T](lhs: T, rhs: T): Boolean = { 112 | (lhs, rhs) match { 113 | case (o: VirtualElementNode, t: VirtualElementNode) => t.closeTo(o) 114 | case p@(_, _) => p._1 == p._2 115 | } 116 | } 117 | 118 | } 119 | 120 | /** 121 | * Virtual node representing an element. Various hooks and hacks help capture 122 | * programmer intent while still allowing a DOM node to be properly configured. 123 | * 124 | * @param tag the element tag 125 | * @param properties key-value pairs. Use "attributes" to enforce using get/set-Attribute otherwise 126 | * property access is used. 127 | * @param children list of children vnodes 128 | * @param key a value used to minimize DOM element node creation. Must be unique among siblings. 129 | */ 130 | case class VirtualElementNode(val tag: String, 131 | val attributes: Seq[KeyValue[_]] = Seq(), 132 | val children: Seq[VNode] = Seq(), 133 | override val key: Option[VNodeKey] = None, 134 | val namespace: Option[String] = None) extends VNode { 135 | 136 | import VNodeUtils._ 137 | 138 | type That = VirtualElementNode 139 | 140 | /** 141 | * Compare to That using only the tag, key, potentially the namespace 142 | * and the number of children if checkChildren is true. 143 | * 144 | */ 145 | def closeTo(that: That, checkChildren: Boolean = false): Boolean = { 146 | val b = tag == that.tag && 147 | (key == that.key) && 148 | (namespace == that.namespace) 149 | if (checkChildren) b && (children.length == that.children.length) 150 | else b 151 | } 152 | 153 | /** 154 | * Diff properties then children and compose the resulting patches. 155 | */ 156 | def diff(that: That, path: Seq[Int] = Nil): Patch = { 157 | if (closeTo(that)) { 158 | // diff properties and children 159 | diffProperties(attributes, that.attributes).applyTo(path) andThen 160 | diffSeq(children, that.children, path) 161 | } else { 162 | // It's not the same node, so just replace it. Very unoptimized :-) 163 | ReplacePatch(that).applyTo(path) 164 | } 165 | } 166 | } 167 | 168 | /** An empty node that renders into something that is backend specific. */ 169 | case class EmptyNode() extends VNode { 170 | type That = EmptyNode 171 | def diff(that: That, path: Seq[Int]) = PathPatch(EmptyPatch, path) 172 | } 173 | 174 | /** 175 | * Control VNode creation from within a VNode. Instead of 176 | * composing your VTree externally using a function, you 177 | * can have subtree generation occur inside the VNode itself. 178 | * This allows you compose tree generation logic 179 | * to another tree without relying on function composition 180 | * external to the tree. Yeah, that sounds useless but it is 181 | * helpful sometimes. 182 | */ 183 | case class ThunkNode(val f: () => VNode) extends VNode { 184 | type That = ThunkNode 185 | def diff(that: That, path: Seq[Int]) = Diff.doDiff(f(), that.f(), path) 186 | } 187 | 188 | /** 189 | * A comment node. Sometimes, comments need to be 190 | * interested into a rendering process e.g. markup generation. 191 | * 192 | * TODO: Comments are all mapped to EmptyPatch. 193 | */ 194 | case class CommentNode(val content: String) extends VNode { 195 | type That = CommentNode 196 | def diff(that: That, path: Seq[Int]) = { 197 | val p = 198 | if (content == that.content) EmptyPatch 199 | else EmptyPatch 200 | if (p != null) p.applyTo(path) 201 | else p 202 | } 203 | } 204 | 205 | /** 206 | * Smart constructors. 207 | */ 208 | object VNode { 209 | 210 | /** 211 | * Create a constant ThunkNode. `f` is evaluated immediately. 212 | */ 213 | def constant(f: => VNode) = { 214 | val c = f 215 | ThunkNode(() => c) 216 | } 217 | 218 | /** 219 | * Create a ThunkNode. 220 | */ 221 | def thunk(f: => VNode) = ThunkNode(() => f) 222 | 223 | /** Create a new virtual text node */ 224 | def tag(text: String) = VirtualElementNode(text) 225 | 226 | /** Create a new virtual element with the given tag */ 227 | def tag(tag: String, attributes: Seq[KeyValue[_]], children: VNode*): VirtualElementNode = 228 | VirtualElementNode(tag, attributes, children) 229 | 230 | /** Create a new virtual element with the given tag and key */ 231 | def tag(tag: String, key: VNodeKey, attributes: Seq[KeyValue[_]], children: VNode*): VirtualElementNode = 232 | VirtualElementNode(tag, attributes, children, Some(key)) 233 | 234 | /** Create a new virtual element with the given tag and key */ 235 | def tag(tag: String, key: VNodeKey, children: VNode*): VirtualElementNode = 236 | VirtualElementNode(tag, Seq(), children, Some(key)) 237 | 238 | /** Create a new virtual element with the given tag, key and namespace */ 239 | def tag(tag: String, key: Option[VNodeKey], namespace: Option[String], attributes: Seq[KeyValue[_]], children: VNode*): VirtualElementNode = 240 | VirtualElementNode(tag, attributes, children, key, namespace) 241 | 242 | /** Create a new virtual element with children, but no attributes. */ 243 | def tag(tag: String, children: VNode*): VirtualElementNode = 244 | VirtualElementNode(tag, Seq(), children) 245 | 246 | /** Create a SVG element. */ 247 | def svg(attributes: Seq[KeyValue[_]], children: VNode*): VirtualElementNode = 248 | VirtualElementNode("svg", attributes, children, None, Some(Constants.NS.SVG)) 249 | 250 | /** 251 | * An empty VNode. 252 | */ 253 | val empty = EmptyNode() 254 | 255 | /** 256 | * Alias for creating a text node. 257 | */ 258 | def text(content: String) = VirtualText(content) 259 | 260 | /** 261 | * Insert a comment. 262 | */ 263 | def comment(content: String) = CommentNode(content) 264 | } 265 | -------------------------------------------------------------------------------- /shared/src/main/scala/im/vdom/backend/Backend.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package backend 19 | 20 | import scala.concurrent.{ Promise, ExecutionContext, Future } 21 | import scala.util.control.NonFatal 22 | 23 | import util.{ Success, Failure, Try } 24 | 25 | /** 26 | * A backend that can run IOActions. 27 | * 28 | * Yes, this looks a little like typesafe's slick. 29 | */ 30 | trait Backend { self => 31 | type Context >: Null <: BasicContext 32 | protected[this]type This >: this.type <: Backend 33 | 34 | final def run[R](a: IOAction[R]): Future[R] = runInternal(a) 35 | 36 | private[vdom] def runInternal[R](a: IOAction[R]): Future[R] = 37 | try runInContext(a, createContext()) catch { case NonFatal(ex) => Future.failed(ex) } 38 | 39 | /** 40 | * Create the default Context for this backend. 41 | */ 42 | protected[this] def createContext(): Context 43 | 44 | /** 45 | * Handle compositional actions and send out patch actions to a subclass overridable 46 | * method. 47 | */ 48 | protected[this] def runInContext[R](a: IOAction[R], ctx: Context): Future[R] = { 49 | logAction(a, ctx) 50 | a match { 51 | case FlatMapAction(base, f, ec) => runInContext(base, ctx).flatMap(v => runInContext(f(v), ctx))(ctx.getEC(ec)) 52 | case AndThenAction(a1, a2) => runInContext(a1, ctx).flatMap(_ => runInContext(a2, ctx))(Action.sameThreadExecutionContext) 53 | case SuccessAction(v) => Future.successful(v) 54 | case FailureAction(t) => Future.failed(t) 55 | case FutureAction(f) => f 56 | case AsTryAction(t) => 57 | val p = Promise[R]() 58 | runInContext(a, ctx).onComplete(v => p.success(v.asInstanceOf[R]))(Action.sameThreadExecutionContext) 59 | p.future 60 | case sa@SequenceAction(actions) => 61 | import java.util.concurrent.atomic.AtomicReferenceArray 62 | val len = actions.length 63 | val results = new AtomicReferenceArray[Any](len) 64 | def run(pos: Int): Future[Any] = { 65 | if (pos == len) Future.successful { 66 | val b = sa.cbf() 67 | var i = 0 68 | while (i < len) { 69 | b += results.get(i) 70 | i += 1 71 | } 72 | b.result() 73 | } 74 | else runInContext(actions(pos), ctx).flatMap { (v: Any) => 75 | results.set(pos, v) 76 | run(pos + 1) 77 | }(Action.sameThreadExecutionContext) 78 | } 79 | run(0).asInstanceOf[Future[R]] 80 | 81 | case CleanUpAction(base, f, keepFailure, ec) => 82 | val p = Promise[R]() 83 | runInContext(base, ctx).onComplete { t1 => 84 | try { 85 | // transform base action result to a flipped Option 86 | val a2 = f(t1 match { 87 | case Success(_) => None 88 | case Failure(t) => Some(t) 89 | }) 90 | // Run the user function that transforms the error 91 | runInContext(a2, ctx).onComplete { t2 => 92 | if (t2.isFailure && (t1.isSuccess || !keepFailure)) p.complete(t2.asInstanceOf[Failure[R]]) 93 | else p.complete(t1) 94 | }(Action.sameThreadExecutionContext) 95 | } catch { 96 | case NonFatal(ex) => 97 | throw (t1 match { 98 | case Failure(t) if keepFailure => t 99 | case _ => ex 100 | }) 101 | } 102 | }(ctx.getEC(ec)) 103 | p.future 104 | case FailedAction(a) => runInContext(a, ctx).failed.asInstanceOf[Future[R]] 105 | case a: ContextualAction[_, _] => runPatchAction(a.asInstanceOf[ContextualAction[R, This]], ctx) 106 | case x@_ => throw new VDomException(s"Unknown action type $x for $this") 107 | } 108 | } 109 | 110 | protected[this] def runPatchAction[R](a: ContextualAction[R, This], ctx: Context): Future[R] = { 111 | val promise = Promise[R]() 112 | try { 113 | val r = a.run(ctx) 114 | promise.success(r) 115 | } catch { 116 | case NonFatal(ex) => promise.tryFailure(ex) 117 | } 118 | promise.future 119 | } 120 | 121 | protected[this] def logAction(a: IOAction[_], ctx: Context): Unit = { 122 | ctx.sequenceCounter += 1 123 | //println(s"#${ctx.sequenceCounter}: $a") 124 | } 125 | 126 | trait BasicContext extends ActionContext { 127 | @volatile private[Backend] var sequenceCounter: Long = 0 128 | 129 | /** 130 | * Given an ExcecutionContext return one, perhaps the modified original or a different one. 131 | */ 132 | private[backend] def getEC(ec: ExecutionContext): ExecutionContext = ec 133 | } 134 | } 135 | 136 | /** 137 | * Backend rendering of VNodes. Since rendering may involve side effects 138 | * or asynchronous activities as a VNode is converted into the output, 139 | * return an IOAction wrapper around the output. Rendering creates 140 | * a description of what should be rendered and the actual processing 141 | * may be delayed until the rendered value is needed. 142 | */ 143 | trait RendererComponent { self => 144 | 145 | /** 146 | * The type output from the rendering. 147 | */ 148 | type RenderOutput 149 | 150 | /** 151 | * Render a VNode producing RenderOutput objects. 152 | * 153 | * The action is a recipe and running it multiple times 154 | * could produce different output values. 155 | */ 156 | def render(vnode: VNode)(implicit executor: ExecutionContext): IOAction[RenderOutput] 157 | } 158 | 159 | /** 160 | * Convert Patches to functions that can be applied to 161 | * Backend specific elements to create IOActions to be run 162 | * by the bBackend. Concrete backends will create ways 163 | * to implicitly create PatchPerformers and 164 | * have transparent syntax to run a Patch, but you can 165 | * also create a PatchPerformer explicitly so you can 166 | * add before and after actions to run using IOAction 167 | * composition. 168 | * 169 | * PatchInput does not necessary have to be 170 | * a specific Element type for the visual UI tree, it could 171 | * be a wrapped object that contains additional information 172 | * that is used to create the IOAction. For example, it could 173 | * contain different types of interceptors/callbacks around 174 | * creating Backend specific elements. 175 | */ 176 | trait PatchesComponent { self => 177 | type PatchInput 178 | type PatchOutput 179 | 180 | /** 181 | * Convert PatchInput into a wrapped PatchOutput. A PatchPerformer 182 | * can be applied to a PatchInput by the user. The output IOAction 183 | * will still need to be run. 184 | */ 185 | trait PatchPerformer extends (PatchInput => IOAction[PatchOutput]) 186 | 187 | object PatchPerformer { 188 | /** Create PatchPerformers more easily */ 189 | def apply(f: PatchInput => IOAction[PatchOutput]) = 190 | new PatchPerformer { 191 | def apply(pi: PatchInput) = f(pi) 192 | } 193 | } 194 | 195 | /** 196 | * Make a patch able to be applied to `PatchInput` to create 197 | * a runnable action. Generally, this function must process 198 | * all Patch types and produce a PatchPerformer object. Concrete 199 | * backends will need to match against the Patch type and 200 | * then create a specialized PatchPerformer. 201 | */ 202 | def makeApplyable(patch: Patch)(implicit executor: ExecutionContext): PatchPerformer 203 | } 204 | 205 | 206 | -------------------------------------------------------------------------------- /shared/src/main/scala/im/vdom/backend/Hints.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package backend 19 | 20 | import scala.collection.immutable.BitSet 21 | import scala.language._ 22 | 23 | /** 24 | * Basic hint structure. Hints are encoded as bit flags..Sub-classes 25 | * are made for different hint usage types so that they can 26 | * be customized with additional usage specific hint information. 27 | * This is essentially like React's DOMProperty hint scaffolding. 28 | */ 29 | trait Hints { 30 | /** Hint information for working with values */ 31 | def values: BitSet 32 | } 33 | 34 | /** 35 | * Hints for working with Elements 36 | */ 37 | case class AttrHint( 38 | val values: BitSet) extends Hints 39 | 40 | /** Hints for Styles. */ 41 | case class StyleHint( 42 | val values: BitSet) extends Hints 43 | 44 | /** 45 | * Hints for Elements. 46 | */ 47 | case class ElHint( 48 | val values: BitSet) extends Hints 49 | 50 | /** 51 | * Some of these hints are only relevant with certain backends, 52 | * such as the DOM backend e.g. MustUseProperty. Some, are 53 | * related to multiple backends, such as HasPositiveNumericValue. 54 | * 55 | * Hints can be combined through '+'. 56 | */ 57 | object Hints { 58 | 59 | /** Convert single BitSet to an AttrHint */ 60 | implicit def hintToAttrHint(hints: BitSet) = AttrHint(values = hints) 61 | 62 | /** Convert multiple BitSets to an AttrHint */ 63 | implicit def hintsToAttrHints(hints: BitSet*) = 64 | AttrHint(values = hints.fold(EmptyHints)(_ & _)) 65 | 66 | /** Empty hint set. */ 67 | val EmptyHints = BitSet.empty 68 | 69 | /** Convenience converter. */ 70 | implicit def toBitSet(i: Int) = BitSet(i) 71 | 72 | /** Should use set/get/remove*Property */ 73 | val MustUseAttribute = 1 74 | 75 | /** Must set value NOt using the attribute API */ 76 | val MustUseProperty = 2 77 | 78 | /** Setting the value has side effects. */ 79 | val HasSideEffects = 3 80 | 81 | /** Whether property should be removed when set to a falsy value. */ 82 | val HasBooleanValue = 4 83 | 84 | /** Not sure what this means in react. */ 85 | val HasOverloadedBooleanValue = 5 86 | 87 | /** Whether property must be numeric or parse as numeric and should be removed when set to a falsy value */ 88 | val HasNumericValue = 6 89 | 90 | /** Numeric and positive. */ 91 | val HasPositiveNumericValue = 7 92 | 93 | // Element hints are separately enumerated from attribute hints. 94 | val OmitClosingTag = 1 95 | val NewlineEating = 2 96 | 97 | // Style hints 98 | val Unitless = 1 99 | 100 | /** 101 | * Empty hints with empty value hints. 102 | */ 103 | val EmptyAttrHints = AttrHint(values = EmptyHints) 104 | } 105 | 106 | trait DOMStyleHints { 107 | import Hints._ 108 | 109 | private val styleHints: Map[String, StyleHint] = Map( 110 | "animationIterationCount" -> StyleHint(values = Unitless), 111 | "boxFlex" -> StyleHint(values = Unitless), 112 | "boxFlexGroup" -> StyleHint(values = Unitless), 113 | "boxOrdinalGroup" -> StyleHint(values = Unitless), 114 | "columnCount" -> StyleHint(values = Unitless), 115 | "flex" -> StyleHint(values = Unitless), 116 | "flexGrow" -> StyleHint(values = Unitless), 117 | "flexPositive" -> StyleHint(values = Unitless), 118 | "flexShrink" -> StyleHint(values = Unitless), 119 | "flexNegative" -> StyleHint(values = Unitless), 120 | "flexOrder" -> StyleHint(values = Unitless), 121 | "gridRow" -> StyleHint(values = Unitless), 122 | "gridColumn" -> StyleHint(values = Unitless), 123 | "fontWeight" -> StyleHint(values = Unitless), 124 | "lineClamp" -> StyleHint(values = Unitless), 125 | "lineHeight" -> StyleHint(values = Unitless), 126 | "opacity" -> StyleHint(values = Unitless), 127 | "order" -> StyleHint(values = Unitless), 128 | "orphans" -> StyleHint(values = Unitless), 129 | "tabSize" -> StyleHint(values = Unitless), 130 | "widows" -> StyleHint(values = Unitless), 131 | "zIndex" -> StyleHint(values = Unitless), 132 | "zoom" -> StyleHint(values = Unitless), 133 | 134 | // SVG-related properties 135 | "fillOpacity" -> StyleHint(values = Unitless), 136 | "stopOpacity" -> StyleHint(values = Unitless), 137 | "strokeDashoffset" -> StyleHint(values = Unitless), 138 | "strokeOpacity" -> StyleHint(values = Unitless), 139 | "strokeWidth" -> StyleHint(values = Unitless)) 140 | 141 | def styleHint(name: String) = styleHints.get(name) 142 | } 143 | protected[backend] object DOMStyleHints extends DOMStyleHints 144 | 145 | trait DOMElHints { 146 | import Hints._ 147 | 148 | private val elHints: Map[String, ElHint] = Map( 149 | "area" -> ElHint(values = OmitClosingTag), 150 | "base" -> ElHint(values = OmitClosingTag), 151 | "br" -> ElHint(values = OmitClosingTag), 152 | "col" -> ElHint(values = OmitClosingTag), 153 | "hr" -> ElHint(values = OmitClosingTag), 154 | "img" -> ElHint(values = OmitClosingTag), 155 | "input" -> ElHint(values = OmitClosingTag), 156 | "keygen" -> ElHint(values = OmitClosingTag), 157 | "listing" -> ElHint(values = NewlineEating), 158 | "link" -> ElHint(values = OmitClosingTag), 159 | "meta" -> ElHint(values = OmitClosingTag), 160 | "param" -> ElHint(values = OmitClosingTag), 161 | "pre" -> ElHint(values = NewlineEating), 162 | "source" -> ElHint(values = OmitClosingTag), 163 | "textarea" -> ElHint(values = NewlineEating), 164 | "track" -> ElHint(values = OmitClosingTag), 165 | "wbr" -> ElHint(values = OmitClosingTag)) 166 | 167 | /** Get a hint or None. */ 168 | def elHint(name: String) = elHints.get(name) 169 | } 170 | 171 | protected[backend] object DOMElHints extends DOMElHints 172 | 173 | trait DOMAttrHints { 174 | import Hints._ 175 | import Constants.NS._ 176 | 177 | private implicit def toAttrHint(i: Int) = AttrHint(values = BitSet(i)) 178 | 179 | private val attrHints: Map[String, AttrHint] = Map( 180 | "allowFullScreen" -> AttrHint(values = BitSet(HasBooleanValue) | BitSet(MustUseAttribute)), 181 | "async" -> AttrHint(values = HasBooleanValue), 182 | "autoPlay" -> AttrHint(values = HasBooleanValue), 183 | "checked" -> AttrHint(values = BitSet(MustUseProperty) | BitSet(HasBooleanValue)), 184 | "class" -> AttrHint(values = MustUseAttribute), 185 | "disabled" -> AttrHint(values = BitSet(MustUseAttribute) | BitSet(HasBooleanValue)), 186 | "download" -> AttrHint(values = HasOverloadedBooleanValue), 187 | "formNoValidate" -> AttrHint(values = HasBooleanValue), 188 | "hidden" -> AttrHint(values = BitSet(MustUseAttribute) | BitSet(HasBooleanValue)), 189 | "height" -> AttrHint(MustUseAttribute), 190 | "hidden" -> AttrHint(MustUseAttribute), 191 | "id" -> AttrHint(MustUseProperty), 192 | "readOnly" -> AttrHint(values = BitSet(MustUseProperty) | BitSet(HasBooleanValue)), 193 | "required" -> AttrHint(values = HasBooleanValue), 194 | "reversed" -> AttrHint(values = HasBooleanValue), 195 | "scoped" -> AttrHint(values = HasBooleanValue), 196 | "seamless" -> AttrHint(values = BitSet(MustUseAttribute) | BitSet(HasBooleanValue)), 197 | "selected" -> AttrHint(values = BitSet(MustUseProperty) | BitSet(HasBooleanValue)), 198 | "value" -> AttrHint(BitSet(MustUseProperty) | BitSet(HasSideEffects)), 199 | "width" -> AttrHint(MustUseAttribute)) 200 | 201 | private val svgHints: Map[String, AttrHint] = Map( 202 | "clipPath" -> AttrHint(MustUseAttribute), 203 | "cx" -> MustUseAttribute, 204 | "cy" -> MustUseAttribute, 205 | "d" -> MustUseAttribute, 206 | "dx" -> MustUseAttribute, 207 | "dy" -> MustUseAttribute, 208 | "fill" -> MustUseAttribute, 209 | "fillOpacity" -> AttrHint(MustUseAttribute), 210 | "fontFamily" -> AttrHint(MustUseAttribute), 211 | "fontSize" -> AttrHint(MustUseAttribute), 212 | "fx" -> MustUseAttribute, 213 | "fy" -> MustUseAttribute, 214 | "gradientTransform" -> AttrHint(MustUseAttribute), 215 | "gradientUnits" -> AttrHint(MustUseAttribute), 216 | "markerEnd" -> AttrHint(MustUseAttribute), 217 | "markerMid" -> AttrHint(MustUseAttribute), 218 | "markerStart" -> AttrHint(MustUseAttribute), 219 | "offset" -> MustUseAttribute, 220 | "opacity" -> MustUseAttribute, 221 | "patternContentUnits" -> MustUseAttribute, 222 | "patternUnits" -> MustUseAttribute, 223 | "points" -> MustUseAttribute, 224 | "preserveAspectRatio" -> MustUseAttribute, 225 | "r" -> MustUseAttribute, 226 | "rx" -> MustUseAttribute, 227 | "ry" -> MustUseAttribute, 228 | "spreadMethod" -> MustUseAttribute, 229 | "stopColor" -> AttrHint(MustUseAttribute), 230 | "stopOpacity" -> AttrHint(MustUseAttribute), 231 | "stroke" -> MustUseAttribute, 232 | "strokeDasharray" -> AttrHint(MustUseAttribute), 233 | "strokeLinecap" -> AttrHint(MustUseAttribute), 234 | "strokeOpacity" -> AttrHint(MustUseAttribute), 235 | "strokeWidth" -> AttrHint(MustUseAttribute), 236 | "textAnchor" -> AttrHint(MustUseAttribute), 237 | "transform" -> MustUseAttribute, 238 | "version" -> MustUseAttribute, 239 | "viewBox" -> AttrHint(MustUseAttribute), 240 | "x1" -> MustUseAttribute, 241 | "x2" -> MustUseAttribute, 242 | "x" -> MustUseAttribute, 243 | "xlinkActuate" -> AttrHint(MustUseAttribute), 244 | "xlinkArcrole" -> AttrHint(MustUseAttribute), 245 | "xlinkHref" -> AttrHint(MustUseAttribute), 246 | "xlinkRole" -> AttrHint(MustUseAttribute), 247 | "xlinkShow" -> AttrHint(MustUseAttribute), 248 | "xlinkTitle" -> AttrHint(MustUseAttribute), 249 | "xlinkType" -> AttrHint(MustUseAttribute), 250 | "xmlBase" -> AttrHint(MustUseAttribute), 251 | "xmlLang" -> AttrHint(MustUseAttribute), 252 | "xmlSpace" -> AttrHint(MustUseAttribute), 253 | "y1" -> MustUseAttribute, 254 | "y2" -> MustUseAttribute, 255 | "y" -> MustUseAttribute) 256 | 257 | /** Get a hint or None. */ 258 | def attrHint(name: String) = attrHints.get(name) orElse svgHints.get(name) 259 | 260 | } 261 | 262 | protected[backend] object DOMAttrHints extends DOMAttrHints 263 | 264 | /** All DOM related hints. */ 265 | trait DOMHints extends DOMStyleHints with DOMElHints with DOMAttrHints 266 | 267 | -------------------------------------------------------------------------------- /shared/src/main/scala/im/vdom/backend/MarkupBackend.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package backend 19 | 20 | import scala.concurrent.ExecutionContext 21 | import scala.concurrent.Future 22 | 23 | /** 24 | * Render to HTML strings. 25 | * 26 | */ 27 | trait MarkupRendererComponent extends RendererComponent { 28 | self: Backend with DOMHints => 29 | 30 | import Utils._ 31 | 32 | type RenderOutput = String 33 | 34 | def render(vnode: VNode)(implicit executor: ExecutionContext): IOAction[RenderOutput] = { 35 | val ctx = createContext().getEC(executor) 36 | 37 | vnode match { 38 | case v@VirtualText(content) => 39 | Action.successful(content) 40 | 41 | case v@VirtualElementNode(tag, attributes, children, key, namespace) => 42 | 43 | // find style "attributes", then add 44 | val processStyles = Action.lift { 45 | val valstr = attributes.filter(keepStyles).map { kv => 46 | createMarkupForStyles(kv, styleHint(kv.key.name)).getOrElse("") 47 | }.mkString(";") 48 | valstr match { 49 | case "" => "" 50 | case v => s"""style="$v;"""" 51 | } 52 | } 53 | 54 | // process remaining attributes 55 | val processAttributes = Action.lift { 56 | attributes.filter(keepAttributes).map { kv => 57 | createMarkupForProperty(kv, attrHint(kv.key.name)).getOrElse("") 58 | }.mkString(" ") 59 | } 60 | 61 | // create tag content 62 | val childMarkup = Action.fold(children.map { child => 63 | render(child) 64 | }, "") { stringAppend } 65 | 66 | val elHint = self.elHint(tag) 67 | 68 | // If omit closing tag, just close tag, don't generate middle content. 69 | val middleEnd = elHint.filter(h => h.values(Hints.OmitClosingTag)).fold { 70 | Seq(Action.successful(">"), 71 | childMarkup, 72 | Action.successful("")) 73 | } { h => Seq(Action.successful("/>")) } 74 | 75 | Action.fold(Seq(Action.successful("<" + tag + " "), 76 | processStyles, 77 | processAttributes) ++ middleEnd, "") { stringAppend } 78 | 79 | case EmptyNode() => 80 | Action.successful("
") 81 | case CommentNode(content) => 82 | Action.successful("") 83 | case ThunkNode(f) => 84 | render(f()) 85 | case x@_ => 86 | Action.failed(new VDomException(s"Unknown VNode type $x for $this")) 87 | } 88 | } 89 | } 90 | 91 | trait MarkupBackend extends Backend with MarkupRendererComponent with DOMHints { 92 | type This = MarkupBackend 93 | type Context = BasicContext 94 | 95 | protected[this] def createContext() = new BasicContext {} 96 | } 97 | 98 | object MarkupBackend extends MarkupBackend 99 | -------------------------------------------------------------------------------- /shared/src/main/scala/im/vdom/backend/Utils.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | package backend 19 | 20 | /** 21 | * Adler32 checksum. 22 | * 23 | * @see http://patterngazer.blogspot.com/2012/01/naive-adler32-example-in-clojure-and.html 24 | */ 25 | private[backend] trait Adler32 { 26 | val base = 65521 27 | 28 | def rebased(value: Int) = value % base 29 | 30 | def cumulated(acc: (Int, Int), item: Byte): (Int, Int) = { 31 | val a = rebased(acc._1 + (item & 0xff)) 32 | (a, (a + acc._2) % base) 33 | } 34 | 35 | def checksum(data: Traversable[Byte]): Int 36 | 37 | def checksumText(data: Traversable[Char]): Int 38 | } 39 | 40 | protected[backend] object Adler32 extends Adler32 { 41 | 42 | override def checksum(data: Traversable[Byte]): Int = { 43 | val result = data.foldLeft((1, 0)) { cumulated(_, _) } 44 | (result._2 << 16) | result._1 45 | } 46 | 47 | def checksumText(data: Traversable[Char]) = { 48 | checksum(data.toSeq.map(_.toByte)) 49 | } 50 | } 51 | 52 | protected[backend] trait Utils { 53 | 54 | /** 55 | * Return Adler32 checksum. 56 | */ 57 | def adler32(str: String): Int = Adler32.checksumText(str) 58 | 59 | /** 60 | * Name of the custom data attribute that stores a checksum. 61 | */ 62 | val ChecksumAttrName = "data-scala-vdom-checksum" 63 | 64 | /** 65 | * Add checksum to end of markup using complete adhoc string regex 66 | * to find the end tag. Assumes the first set of characters 67 | * is the start of an element and the tag ends in ">" or "/>". 68 | */ 69 | def addChecksumToMarkup(markup: String): String = { 70 | val checksum = adler32(markup) 71 | markup.replaceFirst("(/?>)", " " + ChecksumAttrName + "=\"" + checksum + "\"$1") 72 | } 73 | 74 | /** Append two strings */ 75 | def stringAppend(left: String, right: String) = left + right 76 | 77 | /** Filter for filtering attribute lists. Keeps StyleKey attributes. */ 78 | def keepStyles(el: KeyValue[_]) = el match { 79 | case KeyValue(StyleKey(_), _) => true 80 | case _ => false 81 | } 82 | 83 | /** Filter for filtering attribute lists. Keeps AttrKey attributes. */ 84 | def keepAttributes(el: KeyValue[_]) = el match { 85 | case KeyValue(AttrKey(_, _), _) => true 86 | case _ => false 87 | } 88 | 89 | /** 90 | * Take a value and convert it to a quoted string suitable for use 91 | * as an attribute's value in markup. 92 | */ 93 | def quoteValueForBrowser[T](v: T): String = 94 | "\"" + escapeTextForBrowser(v) + "\"" 95 | 96 | private[this] val escapeTable = Seq( 97 | ("&".r, "&"), 98 | (">".r, ">"), 99 | ("<".r, "<"), 100 | ("\"".r, """), 101 | ("'".r, "'")) 102 | 103 | /** 104 | * Escape certain char sequences to avoid scripting attacks. 105 | * 106 | * TODO: Horribly inefficient! 107 | */ 108 | def escapeTextForBrowser[T](v: T): String = { 109 | var rval = v.toString 110 | for (p <- escapeTable) 111 | rval = p._1.replaceAllIn(rval, p._2) 112 | rval 113 | } 114 | 115 | /** 116 | * Check hint structure and apply business rule about whether this 117 | * value should be ignored and does not need to be set into a DOM object. 118 | */ 119 | def ignoreValue[T](key: KeyPart, hintopt: Option[AttrHint], value: T): Boolean = { 120 | (hintopt.map(_.values).getOrElse(Hints.EmptyHints), value) match { 121 | case (hints, false) if (hints(Hints.HasBooleanValue)) => true 122 | case _ => false 123 | } 124 | } 125 | 126 | /** 127 | * Create markup for a key value pair. It considers both the hint and the value 128 | * when generating markup. 129 | * 130 | * If no hint is found, generate a simple `name = 'value'` and convert the 131 | * value to a quoted string. If the value is None then it 132 | * generates a None return value. 133 | * 134 | * @return None if no markup was generated or a string of markup. 135 | */ 136 | def createMarkupForProperty(kv: KeyValue[_], hintopt: Option[AttrHint]): Option[String] = { 137 | kv.value. 138 | filterNot(ignoreValue(kv.key, hintopt, _)). 139 | map { v => 140 | (hintopt.map(_.values).getOrElse(Hints.EmptyHints), v) match { 141 | case (hints, true) if (hints(Hints.HasBooleanValue) || hints(Hints.HasOverloadedBooleanValue)) => 142 | kv.key.name + """=""""" 143 | case _ => kv.key.name + "=" + quoteValueForBrowser(v) 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * Take a style value and prepare it to be inserted into markup. 150 | */ 151 | def quoteStyleValueForBrowser[T](hintopt: Option[StyleHint], v: T) = { 152 | (hintopt.map(_.values).getOrElse(collection.BitSet.empty), v) match { 153 | case (_, null) => "" 154 | case (_, true) => "" 155 | case (_, false) => "" 156 | case (_, "") => "" 157 | case (hints, x@_) if (hints(Hints.Unitless)) => v.toString 158 | case _ => v.toString.trim + "px" 159 | } 160 | } 161 | 162 | /** Convert camel cased to a hyphenated name. */ 163 | def hyphenate(name: String) = name.replaceAll("([A-Z])", "-$1").toLowerCase 164 | 165 | /** 166 | * Process a style name for proper formation. Hyphenate and fix 167 | * some. 168 | * 169 | * For "ms-" prefix convert to "-ms-" per react. 170 | */ 171 | def processStyleName(name: String) = { 172 | hyphenate(name).trim.replaceAll("^ms-", "-ms-") 173 | } 174 | 175 | /** 176 | * Create style markup. This does NOT include `style=` or 177 | * surrounding quotes. None values conceptually indicate 178 | * we should ignore the value so no markup is generated for 179 | * keys with None values. 180 | * 181 | * @return None if no markup was generated or a string of markup. 182 | * 183 | * TODO Allow the specification of a hint source. 184 | */ 185 | def createMarkupForStyles(kv: KeyValue[_], hintopt: Option[StyleHint]): Option[String] = 186 | kv.value.map { v => processStyleName(kv.key.name) + ":" + quoteStyleValueForBrowser(hintopt, v) } 187 | 188 | /** 189 | * Norm the string for comparison purposes. Remove repeated whitespace, 190 | * remove whitespace before non-alpha characters, remove leading/trailing 191 | * whitespace. Note that newlines are stripped as well and cannot 192 | * detect correctness if a newline MUST be in the normalized string. 193 | */ 194 | def norm(input: String): String = { 195 | input.trim(). 196 | replaceAll("(\\s)+", " "). 197 | replaceAll("\\s(\\W)", "$1"). 198 | replaceAll("(\\W)\\s", "$1"). 199 | toUpperCase 200 | } 201 | 202 | } 203 | 204 | protected[backend] object Utils extends Utils -------------------------------------------------------------------------------- /shared/src/main/scala/im/vdom/package.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | 18 | import scala.language._ 19 | 20 | package object vdom { 21 | 22 | /** 23 | * Virtual nodes may have keys to improve diff'ing performance. 24 | */ 25 | type VNodeKey = String 26 | 27 | /** 28 | * Reduction to a single patch object using composition. All 29 | * patches will apply to the same input object when applied. 30 | */ 31 | implicit def seqPatchToPatch(seq: Seq[Patch]): Patch = seq.fold(EmptyPatch)((p, n) => p andThen n) 32 | 33 | /** 34 | * Generate a Patch that describes the differences between original and target. 35 | */ 36 | def diff(original: VNode, target: VNode): Patch = Diff.diff(original, target) 37 | 38 | /** 39 | * Helpers for comparing Option values. 40 | */ 41 | implicit class OptionOps[T](lhs: Option[T]) { 42 | 43 | /** 44 | * rhs = None acts like a wildcard and matches anything 45 | * on lhs. But lhs = None only matches a rhs None. 46 | */ 47 | def wildcardEq(rhs: Option[T]) = (lhs, rhs) match { 48 | case (None, None) => true 49 | case (None, _) => false 50 | case (_, None) => true 51 | case (Some(l), Some(r)) => l == r 52 | } 53 | 54 | /** 55 | * Negated `===` 56 | */ 57 | def /==(rhs: Option[T]) = !(===(rhs)) 58 | /** 59 | * Equal only if they are both defined and the defined values are equal. 60 | */ 61 | def ===(rhs: Option[T]) = lhs.toRight(false) == rhs.toRight(true) 62 | } 63 | 64 | /** 65 | * Enable explicit `.toPatch` notation on a sequence of patches. 66 | */ 67 | implicit class ToPatch(seq: Seq[Patch]) { 68 | def to1Patch = seqPatchToPatch(seq) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /shared/src/test/scala/im/vdom/DiffSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import org.scalatest._ 20 | 21 | /** 22 | * Test diffing algorithm. 23 | */ 24 | class DiffSpec extends FlatSpec 25 | with Matchers 26 | with OptionValues { 27 | 28 | import VNode._ 29 | import VNodeUtils._ 30 | 31 | val nodea = Seq( 32 | tag("li", text("one")), 33 | tag("li", text("two")), 34 | tag("li", text("three"))) 35 | 36 | val nodeb = Seq( 37 | tag("li", text("one")), 38 | tag("li", text("two")), 39 | tag("li", text("three")), 40 | tag("li", text("four"))) 41 | 42 | val nodeBChangeLastVNode = Seq( 43 | tag("li", text("one")), 44 | tag("li", text("two")), 45 | tag("li", text("three")), 46 | tag("p")) 47 | 48 | "General vnode tests" should "show two lists of line items to be not equal if they have different content" in { 49 | nodea should not equal (nodeb) 50 | } 51 | 52 | it should "show two ul with the same number of children to be close to but may not be equal" in { 53 | val nodea = tag("ul", 54 | tag("li", text("foo")), 55 | tag("li", text("fi")), 56 | tag("li", text("fee"))) 57 | val nodeb = tag("ul", 58 | tag("li", text("foo")), 59 | tag("li", text("fi")), 60 | tag("li", text("fee"))) 61 | assert(nodea closeTo nodeb) 62 | } 63 | 64 | it should "two ul with the same number of children but different keys to not be close to" in { 65 | val nodea = tag("ul", "key1", Seq(), 66 | tag("li", text("foo")), 67 | tag("li", text("fi")), 68 | tag("li", text("fee"))) 69 | val nodeb = tag("ul", "key2", Seq(), 70 | tag("li", text("foo")), 71 | tag("li", text("fi")), 72 | tag("li", text("fee"))) 73 | nodea closeTo nodeb should not be (true) 74 | } 75 | 76 | it should "correctly find or not find a vnode in a list of vnodes using kindOfContains equals and closeEnough" in { 77 | val x = kindOfContains(nodeb, nodeb(3))(_ == _) 78 | x should be(true) 79 | 80 | val y = kindOfContainsCloseEnough(nodeb, nodeb(3)) 81 | y should be(true) 82 | 83 | val z = kindOfContains(nodeb, tag("li", text("boom")))(_ == _) 84 | z should be(false) 85 | 86 | val ztoo = kindOfContainsCloseEnough(nodeb, tag("li", text("boom"))) 87 | ztoo should be(true) 88 | 89 | val znot = kindOfContainsCloseEnough(nodeb, tag("li")) 90 | znot should be(true) 91 | } 92 | 93 | it should "find removes using equality but not find any using closeEnough when using similar vnodes" in { 94 | val x = findRemoves(nodeb, nodea)(_ == _) 95 | x should have length 1 96 | x should contain(3) 97 | 98 | val y = findRemovesCloseEnough(nodeb, nodea) 99 | withClue("find removes using closeEnough") { 100 | y should have length 0 101 | } 102 | } 103 | 104 | it should "find removes using equality and closeEnough when using non-similar vnodes" in { 105 | val x = findRemoves(nodeBChangeLastVNode, nodea)(_ == _) 106 | x should have length 1 107 | x should contain(3) 108 | 109 | val y = findRemovesCloseEnough(nodeBChangeLastVNode, nodeb) 110 | withClue("find removes using closeEnough") { 111 | y should have length 1 112 | } 113 | } 114 | 115 | it should "not find removes comparing with equality and closeEnough and comparing a sequence to itself" in { 116 | val x = findRemoves(nodeb, nodeb)(_ == _) 117 | x should have length 0 118 | 119 | val y = findRemovesCloseEnough(nodeb, nodeb) 120 | withClue("find removes using closeEnough") { 121 | y should have length 0 122 | } 123 | } 124 | 125 | it should "find adds using exact matches but not with closeEnough" in { 126 | val x = findAdds(nodea, nodeb)(_ == _) 127 | x should have length 1 128 | x(0) should equal(3) 129 | 130 | val y = findRemovesCloseEnough(nodea, nodeb) 131 | withClue("find adds using closeEnough") { 132 | y should have length 0 133 | } 134 | } 135 | 136 | "diffSeq" should "should find the removes when one vnode is removed in a sequence" in { 137 | val (moves, removes, adds, rest) = Diff.diffSeq2(nodeb, nodea, Nil) 138 | removes should have length 1 139 | } 140 | 141 | it should "should find the adds when one vnode is removed in a sequence" in { 142 | val (moves, removes, adds, rest) = Diff.diffSeq2(nodea, nodeb, Nil) 143 | adds should have length 1 144 | } 145 | 146 | it should "find simple moves if two children swap places" in { 147 | val lhs = Seq(nodea(0), nodea(1)) 148 | val rhs = Seq(nodea(1), nodea(0)) 149 | val (moves, removes, adds, rest) = Diff.diffSeq2(lhs, rhs, Nil) 150 | withClue("moves") { moves should have length 2 } 151 | withClue("removes") { removes should have length 0 } 152 | withClue("adds") { adds should have length 0 } 153 | } 154 | 155 | it should "find simple moves if two children change places" in { 156 | val lhs = Seq(nodea(0), nodea(1), nodea(2)) 157 | val rhs = Seq(nodea(2), nodea(1), nodea(0)) 158 | val (moves, removes, adds, rest) = Diff.diffSeq2(lhs, rhs, Nil) 159 | withClue("moves") { moves should have length 2 } 160 | withClue("removes") { removes should have length 0 } 161 | withClue("adds") { adds should have length 0 } 162 | } 163 | 164 | it should "find simple removes and moves if two children change place and one child is dropped" in { 165 | val lhs = Seq(nodea(0), nodea(1), nodea(2)) 166 | val rhs = Seq(nodea(2), nodea(1)) 167 | val (moves, removes, adds, rest) = Diff.diffSeq2(lhs, rhs, Nil) 168 | withClue("moves") { 169 | moves should have length 2 170 | moves(0) should equal((1, 0)) 171 | moves(1) should equal((0, 1)) 172 | } 173 | withClue("removes") { 174 | removes should contain only (0) 175 | } 176 | withClue("adds") { 177 | adds should have length 0 178 | } 179 | } 180 | 181 | it should "find simple adds and moves if two children change place and one child is dropped" in { 182 | val lhs = Seq(nodea(0), nodea(1)) 183 | val rhs = Seq(nodea(1), nodea(0), nodea(2)) 184 | val (moves, removes, adds, rest) = Diff.diffSeq2(lhs, rhs, Nil) 185 | withClue("moves") { 186 | moves should have length 2 187 | moves(0) should equal((1, 0)) 188 | moves(1) should equal((0, 1)) 189 | } 190 | withClue("removes") { 191 | removes should have length 0 192 | } 193 | withClue("adds") { 194 | adds should contain only (2) 195 | } 196 | } 197 | 198 | "Diff" should "create an empty patch of both vnodes are the same" in { 199 | val nodea = tag("p") 200 | val nodeb = tag("p") 201 | val d = diff(nodea, nodeb) 202 | d should equal(EmptyPatch) 203 | } 204 | 205 | it should "create an empty patch with a multi-level vnode tree when the trees are the same" in { 206 | val d = diff(tag("ul", nodea: _*), tag("ul", nodea: _*)) 207 | d should equal(EmptyPatch) 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /shared/src/test/scala/im/vdom/OptionSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | //import _root_.org.scalajs.{ dom => d } 20 | 21 | import org.scalatest.FlatSpec 22 | import org.scalatest.Matchers 23 | import org.scalatest.OptionValues 24 | import org.scalatest.prop 25 | 26 | /** 27 | * Test OptionOpps. 28 | */ 29 | class OptionSpec extends FlatSpec 30 | with Matchers 31 | with OptionValues 32 | with prop.TableDrivenPropertyChecks { 33 | 34 | import im.vdom.OptionOps 35 | 36 | val goodwildcard = Table( 37 | ("lhs", "rhs"), 38 | (None, None), 39 | (Some(1), None), 40 | (Some("blah"), None)) 41 | 42 | it should "find good wildCardEq" in { 43 | forAll(goodwildcard) { (lhs: Option[Any], rhs: Option[Any]) => 44 | lhs wildcardEq rhs should equal(true) 45 | } 46 | } 47 | 48 | val badwildcard = Table( 49 | ("lhs", "rhs"), 50 | (None, Some(1)), 51 | (Some(3), Some(4))) 52 | 53 | it should "find bad wildCardEq" in { 54 | forAll(badwildcard) { (lhs: Option[Any], rhs: Option[Any]) => 55 | lhs wildcardEq rhs should equal(false) 56 | } 57 | } 58 | 59 | it should "handle ===" in { 60 | Some(3) === Some(3) should equal (true) 61 | None === None should equal (false) 62 | None === Some(3) should equal (false) 63 | Some(4) === Some(5) should equal (false) 64 | } 65 | 66 | 67 | } 68 | 69 | -------------------------------------------------------------------------------- /shared/src/test/scala/im/vdom/VNodeSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2015 Devon Miller 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package im 17 | package vdom 18 | 19 | import org.scalatest._ 20 | 21 | /** 22 | * Patches do not need much testing...so this is really 23 | * just a dummy test class. 24 | */ 25 | class VNodeSpec extends FlatSpec 26 | with Matchers 27 | with OptionValues { 28 | 29 | import VNode._ 30 | 31 | "VNode" should "have equals that works" in { 32 | val lhs = tag("div", Some("key1"), None, Seq()) 33 | val rhs = tag("div", Some("key1"), None, Seq()) 34 | val rhs2 = tag("div", Some("key2"), None, Seq()) 35 | 36 | assertResult(true)(lhs == rhs) 37 | assertResult(false)(rhs == rhs2) 38 | } 39 | 40 | it should "equals on a simple tag" in { 41 | val lhs = tag("div") 42 | val rhs = tag("div") 43 | assertResult(true)(lhs == rhs) 44 | assertResult(false)(lhs eq rhs) 45 | } 46 | 47 | "closeTo" should "test on tag, key and namespace" in { 48 | val lhs = tag("div", None, None, Seq()) 49 | val rhs = tag("div", None, None, Seq()) 50 | assertResult(true)(lhs.closeTo(rhs)) 51 | 52 | val lhs1 = tag("div", Some("k1"), None, Seq()) 53 | val rhs1 = tag("div", Some("k1"), None, Seq()) 54 | assertResult(true)(lhs1.closeTo(rhs1)) 55 | 56 | val rhs2 = tag("div", None, None, Seq()) 57 | assertResult(false)(lhs1.closeTo(rhs2)) 58 | 59 | val lhs3 = tag("div", Some("k1"), Some("ns"), Seq()) 60 | val rhs3 = tag("div", Some("k1"), Some("ns"), Seq()) 61 | assertResult(true)(lhs3.closeTo(rhs3)) 62 | 63 | val rhs4 = tag("div", Some("k1"), None, Seq()) 64 | assertResult(false)(lhs3.closeTo(rhs4)) 65 | } 66 | 67 | "closeToOrEqual" should "use closeTo when a VirtualElementNode and == when anything else" in { 68 | import VNodeUtils.closeToOrEquals 69 | val lhs = text("test") 70 | val rhs = text("test") 71 | assertResult(true)(closeToOrEquals(lhs, rhs)) 72 | 73 | val rhs2 = comment("comment") 74 | assertResult(false)(lhs.closeToOrEquals(rhs2)) 75 | 76 | val lhs3 = tag("div", Some("k1"), Some("ns"), Seq()) 77 | val rhs3 = tag("div", Some("k1"), Some("ns"), Seq()) 78 | assertResult(true)(closeToOrEquals(lhs3, rhs3)) 79 | 80 | assertResult(false)(closeToOrEquals(lhs, rhs3)) 81 | } 82 | 83 | "kindOfContains" should "find something close in the list" in { 84 | val seq = Seq(text("text"), tag("div", Some("k1"), None, Seq()), comment("comment")) 85 | assertResult(true)(VNodeUtils.kindOfContainsCloseEnough(seq, tag("div", Some("k1"), None, Seq()))) 86 | assertResult(true)(VNodeUtils.kindOfContainsCloseEnough(seq, comment("comment"))) 87 | assertResult(false)(VNodeUtils.kindOfContainsCloseEnough(seq, tag("div", Some("k1"), Some("ns"), Seq()))) 88 | } 89 | 90 | "findRemoves" should "find removes in easy vnode list" in { 91 | val lhs = Seq(text("t1"), text("t2"), text("t3")) 92 | val rhs = Seq(text("t1"), text("t3")) 93 | val r = VNodeUtils.findRemovesCloseEnough(lhs, rhs) 94 | assertResult(1)(r.size) 95 | assertResult(1)(r(0)) 96 | } 97 | 98 | it should "not remove nodes if they are close to each other with keys" in { 99 | val lhs = Seq(text("t1"), tag("div", Some("k1"), None, Seq(), text("text")), text("t3")) 100 | val rhs = Seq(text("t1"), tag("div", Some("k1"), None, Seq()), text("t3")) 101 | val r = VNodeUtils.findRemovesCloseEnough(lhs, rhs) 102 | assertResult(0)(r.size) 103 | } 104 | 105 | it should "not remove nodes if they are close to each without keys" in { 106 | val lhs = Seq(text("t1"), tag("div", None, None, Seq(), text("text")), text("t3")) 107 | val rhs = Seq(text("t1"), tag("div", None, None, Seq()), text("t3")) 108 | val r = VNodeUtils.findRemovesCloseEnough(lhs, rhs) 109 | assertResult(0)(r.size) 110 | } 111 | 112 | it should "not find any removes on empty lists" in { 113 | val rhs: Seq[VNode] = Seq.empty 114 | val lhs: Seq[VNode] = Seq.empty 115 | assertResult(0)(VNodeUtils.findRemovesCloseEnough(lhs, rhs).size) 116 | } 117 | 118 | it should "find 1 remove when the target is empty and source has 1 el" in { 119 | val lhs = Seq(text("t")) 120 | assertResult(1)(VNodeUtils.findRemovesCloseEnough(lhs, Seq.empty).size) 121 | } 122 | 123 | it should "find 1 remove when the target has 1 el different from the source" in { 124 | val rhs = Seq(text("tnew")) 125 | val lhs = Seq(text("told")) 126 | assertResult(1)(VNodeUtils.findRemovesCloseEnough(lhs, rhs).size) 127 | } 128 | 129 | "findAdds" should "find adds in easy vnode list" in { 130 | val rhs = Seq(text("t1"), text("t2"), text("t3")) 131 | val lhs = Seq(text("t1"), text("t3")) 132 | val r = VNodeUtils.findAddsCloseEnough(lhs, rhs) 133 | assertResult(1)(r.size) 134 | assertResult(1)(r(0)) 135 | } 136 | 137 | } -------------------------------------------------------------------------------- /shared/src/test/scala/im/vdom/backend/BackendSpec.scala: -------------------------------------------------------------------------------- 1 | /* Copyright 2015 Devon Miller 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package im 16 | package vdom 17 | package backend 18 | 19 | import scala.language._ 20 | import org.scalatest._ 21 | import scala.concurrent.Future 22 | import scala.concurrent.ExecutionContext.Implicits.global 23 | import scala.concurrent.duration._ 24 | import java.util.concurrent.TimeUnit 25 | import java.util.concurrent.atomic.AtomicInteger 26 | 27 | /** 28 | * A base DOM backend that can be extended with specific components as needed. 29 | * This trait defines the context type. 30 | */ 31 | trait TestBackend extends Backend { 32 | type This = TestBackend 33 | type Context = BasicContext 34 | 35 | protected[this] def createContext() = new BasicContext {} 36 | } 37 | 38 | // Could just do `new TestBackend {}` 39 | class MyTestBackend extends TestBackend 40 | 41 | /** 42 | * A test backend is used to test IOAction run funcitonality. 43 | */ 44 | class BackendSpec extends AsyncFlatSpec with Matchers with OptionValues { 45 | 46 | val b = new MyTestBackend() 47 | 48 | "Backend" should "run a successful action" in { 49 | val action = Action.successful(true) 50 | val result = b.run(action) 51 | result.map { result => 52 | result should be(true) 53 | } 54 | } 55 | 56 | it should "contain the exception when projecting with action.failed " in { 57 | val ex = new IllegalArgumentException("bad arg") 58 | val action = Action.failed(ex) 59 | recoverToSucceededIf[IllegalArgumentException] { b.run(action) } 60 | } 61 | 62 | it should "throw a NoSuchElement exception when the result in action.failed does not contain an exception" in { 63 | recoverToSucceededIf[NoSuchElementException] { b.run(Action.successful(10).failed) } 64 | } 65 | 66 | it should "return a future value" in { 67 | val f = Future(1) 68 | val action = Action.from(f) 69 | val result = b.run(action) 70 | result.map { r => 71 | r should be(1) 72 | } 73 | } 74 | 75 | it should "map" in { 76 | val action = Action.successful(1).map(_ + 1) 77 | val result = b.run(action) 78 | result.map { r => 79 | r should be(2) 80 | } 81 | } 82 | 83 | it should "flatMap" in { 84 | val action = Action.successful(1) 85 | val action2 = action.flatMap(n => Action.successful(n + 1)) 86 | val result = b.run(action2) 87 | result.map { r => 88 | r should be(2) 89 | } 90 | } 91 | 92 | it should "sequence actions, run them, return Unit" in { 93 | val r = new AtomicInteger(0) 94 | val actions = (1 to 10).map(n => ContextualAction { 95 | r.incrementAndGet() 96 | n 97 | }) 98 | val result = b.run(Action.seq(actions: _*)) 99 | result.map { res => 100 | res should be(()) 101 | } 102 | r.get should be(10) 103 | 104 | val actions2 = (1 to 10).map(n => 105 | ContextualAction[Int, MyTestBackend] { ctx: MyTestBackend#Context => 106 | r.incrementAndGet() 107 | n 108 | }) 109 | val action2 = Action.seq(actions2: _*) 110 | val result2 = b.run(action2) 111 | result2.map { res => 112 | res should be(()) 113 | r.get should be(20) 114 | } 115 | } 116 | 117 | it should "allow easy extraction to a value" in { 118 | val x = Action.successful(Some(10)) 119 | val y = x.map(_.get) 120 | val r = b.run(y) 121 | r.map { res => 122 | res should be(10) 123 | } 124 | 125 | val extracted = x.flatMap { opt => 126 | opt match { 127 | case Some(x) => Action.successful(x) 128 | case _ => Action.failed(new NoSuchElementException("no el")) 129 | } 130 | } 131 | b.run(extracted).map { res => 132 | res should equal(10) 133 | } 134 | } 135 | 136 | it should "project using action.failed correctly" in { 137 | val x = Action.failed(new IllegalArgumentException("blah")).failed 138 | b.run(x).map { ex => 139 | ex.getMessage should equal("blah") 140 | } 141 | } 142 | 143 | it should "project using action.failed correctly when there is no exception thrown" in { 144 | val x = Action.successful(10).failed 145 | recoverToSucceededIf[NoSuchElementException] { 146 | b.run(x) 147 | } 148 | } 149 | 150 | it should "always call finally if action was successful anyway" in { 151 | val counter = new AtomicInteger(0) 152 | 153 | val af = Action.successful(10).andFinally(Action.lift { 154 | counter.incrementAndGet() 155 | }) 156 | b.run(af).map { res => 157 | counter.get should equal(1) 158 | res should equal(10) 159 | } 160 | } 161 | it should "always call finally if action was failed" in { 162 | val counter = new AtomicInteger(0) 163 | 164 | val a2 = Action.failed(new IllegalArgumentException("blah")) 165 | val af2 = a2.andFinally(ContextualAction { 166 | counter.incrementAndGet() 167 | }) 168 | recoverToSucceededIf[IllegalArgumentException] { 169 | val f = b.run(af2) 170 | counter.get should equal(1) 171 | f 172 | } 173 | } 174 | 175 | it should "propagate base action's failure when using finally" in { 176 | val counter = new AtomicInteger(0) 177 | 178 | val a: IOAction[Int] = Action.failed(new IllegalArgumentException("blah")) 179 | val af = a.andFinally(ContextualAction { 180 | counter.incrementAndGet() 181 | }) 182 | val f = b.run(af) 183 | recoverToSucceededIf[IllegalArgumentException] { 184 | counter.get should be(1) 185 | f 186 | } 187 | } 188 | 189 | it should "call cleanup with None when successful" in { 190 | val counter = new AtomicInteger(0) 191 | 192 | val x = Action.successful(Some(10)) 193 | val y = x.map(_.get) 194 | 195 | val cleanup = y.cleanUp { err: Option[Throwable] => 196 | err match { 197 | case Some(t) => 198 | fail("base action was successful so this should not be called") 199 | case _ => 200 | counter.incrementAndGet() // should run 201 | Action.successful(-1) // should never see it! 202 | } 203 | } 204 | b.run(cleanup).map { res => 205 | res should be(10) 206 | counter.get should be(1) 207 | } 208 | } 209 | 210 | it should "call cleanup with Some(ex) when failure occurs" in { 211 | val counter = new AtomicInteger(0) 212 | // Acually throw an exception, could use Action.failed to create this... 213 | val x = Action.lift { throw new IllegalArgumentException("blah") } 214 | val cleanup = x.cleanUp { err: Option[Throwable] => 215 | err match { 216 | case Some(t) => 217 | counter.incrementAndGet() // should run 218 | Action.successful(10) 219 | case None => 220 | fail("base action was failed so this should not be called") 221 | } 222 | } 223 | // Small track, you need the projection, otherwise the exception would be thrown during run! 224 | b.run(cleanup.failed).map { ex => 225 | counter.get should be(1) 226 | } 227 | } 228 | 229 | it should "run the cleanup action even if base action was successful" in { 230 | val counter = new AtomicInteger(0) 231 | val cleanup = Action.successful(10).cleanUp { err: Option[Throwable] => 232 | err match { 233 | case Some(t) => 234 | fail("base action was successful so this should not be called") 235 | case None => 236 | Action.lift(counter.incrementAndGet) 237 | } 238 | } 239 | b.run(cleanup).map { res => 240 | counter.get should be(1) 241 | } 242 | } 243 | 244 | it should "run the cleanup action even if base action fails" in { 245 | val counter = new AtomicInteger(0) 246 | val cleanup = Action.lift{ throw new IllegalArgumentException("blah")}.cleanUp { err: Option[Throwable] => 247 | err match { 248 | case Some(t) => 249 | Action.lift(counter.incrementAndGet) 250 | case None => 251 | fail("base action was successful so this should not be called") 252 | } 253 | } 254 | b.run(cleanup.failed).map { res => 255 | counter.get should be(1) 256 | } 257 | } 258 | 259 | it should "pass exception from base action to cleanup when keepFailure=true" in { 260 | // slight change in style, we actually throw the exception instead of using Action.failed 261 | val baseAction: IOAction[Int] = Action.lift { throw new IllegalArgumentException("ouch!") } 262 | val cleanup: IOAction[Int] = baseAction 263 | .cleanUp { err: Option[Throwable] => 264 | err match { 265 | case Some(t) => 266 | Action.failed(new NoSuchElementException()) 267 | case _ => 268 | fail("base action was failed so this should not be called") 269 | } 270 | } 271 | recoverToSucceededIf[IllegalArgumentException] { 272 | b.run(cleanup) 273 | } 274 | } 275 | 276 | it should "return cleanup action's exception if base fails, cleanUp fails and keepFailure=false" in { 277 | // lazy many style to create a failed action 278 | val baseAction: IOAction[Int] = Action.failed(new IllegalArgumentException("ouch!")) 279 | val cleanup: IOAction[Int] = baseAction.cleanUp({ 280 | _ match { 281 | case Some(t) => Action.failed(new NoSuchElementException()) 282 | case _ => fail("base action was failed so this should not be called") 283 | } 284 | }, false) 285 | recoverToSucceededIf[NoSuchElementException] { b.run(cleanup) } 286 | } 287 | 288 | } 289 | -------------------------------------------------------------------------------- /shared/src/test/scala/im/vdom/backend/MarkupBackendSpec.scala: -------------------------------------------------------------------------------- 1 | /* Copyright 2015 Devon Miller 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at9 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package im 16 | package vdom 17 | package backend 18 | 19 | import scala.language._ 20 | import org.scalatest._ 21 | import scala.concurrent.Future 22 | import scala.concurrent.ExecutionContext.Implicits.global 23 | import scala.concurrent.duration._ 24 | import java.util.concurrent.TimeUnit 25 | import java.util.concurrent.atomic.AtomicInteger 26 | 27 | /** 28 | * A test backend is used to test IOAction run funcitonality. 29 | */ 30 | class MarkupBackendSpec extends AsyncFlatSpec with Matchers with OptionValues { 31 | 32 | import Utils._ 33 | import VNode._ 34 | 35 | val b = MarkupBackend 36 | import HTML5Attributes._ 37 | 38 | "MarkupBackend" should "run a successful action" in { 39 | val action = Action.successful(true) 40 | val result = b.run(action) 41 | result.map { result => 42 | result should be(true) 43 | } 44 | } 45 | 46 | it should "render a simple tag" in { 47 | val v = tag("div") 48 | b.run(b.render(v)).map { html => 49 | norm(html) should be("
") 50 | } 51 | } 52 | 53 | it should "render a tag with insane whitespace around it" in { 54 | val v = tag(" div \t\n ") 55 | b.run(b.render(v)).map { html => 56 | norm(html) should be("
") 57 | } 58 | } 59 | 60 | it should "render a tag with a string attribute" in { 61 | val v = tag("div", Seq(id := "foo")) 62 | b.run(b.render(v)).map { html => 63 | norm(html) should be("""
""") 64 | } 65 | } 66 | 67 | it should "render a tag with a string attribute and a text node" in { 68 | val v = tag("div", Seq(id := "foo"), text("FOO")) 69 | b.run(b.render(v)).map { html => 70 | norm(html) should equal("""
FOO
""") 71 | } 72 | } 73 | 74 | it should "render a tage with child elements" in { 75 | val v = tag("div", tag("p", text("foo")), tag("div", text("bar"))) 76 | b.run(b.render(v)).map { html => 77 | norm(html) should equal("""

FOO

BAR
""") 78 | } 79 | } 80 | 81 | it should "omit a closing tag for br" in { 82 | val v = tag("br") 83 | b.run(b.render(v)).map { html => 84 | norm(html) should equal("""
""") 85 | } 86 | } 87 | 88 | it should "not render an HasBooleanValue=true attribute when the attribute is false" in { 89 | val v = tag("div", Seq(disabled := false)) 90 | b.run(b.render(v)).map { html => 91 | norm(html) should equal("""
""") 92 | } 93 | } 94 | 95 | // Not sure this should render to ="" or to just the name of the attribute per HTML5 spec 96 | it should "render a HasBooleanValue=true attribute when the attribute is true" in { 97 | val v = tag("div", Seq(disabled := true)) 98 | b.run(b.render(v)).map { html => 99 | norm(html) should equal("""
""") 100 | } 101 | } 102 | 103 | it should "render a simple style" in { 104 | val v = tag("div", Seq(Styles.height := 200)) 105 | b.run(b.render(v)).map { html => 106 | norm(html) should equal ("""
""") 107 | } 108 | } 109 | 110 | it should "render multple styles" in { 111 | val v = tag("div", Seq(Styles.height := 200, Styles.width := 400, Styles.zIndex := 3)) 112 | b.run(b.render(v)).map { html => 113 | norm(html) should equal ("""
""") 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /shared/src/test/scala/im/vdom/backend/UtilsSpec.scala: -------------------------------------------------------------------------------- 1 | /* Copyright 2015 Devon Miller 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at9 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | package im 16 | package vdom 17 | package backend 18 | 19 | import scala.language._ 20 | import org.scalatest._ 21 | import scala.concurrent.Future 22 | import scala.concurrent.ExecutionContext.Implicits.global 23 | import scala.concurrent.duration._ 24 | import java.util.concurrent.TimeUnit 25 | import java.util.concurrent.atomic.AtomicInteger 26 | 27 | /** 28 | * Utils test. 29 | */ 30 | class UtilsSpec extends FlatSpec with Matchers with OptionValues { 31 | 32 | import Utils._ 33 | import collection.immutable.BitSet 34 | import im.vdom.backend.Hints; 35 | 36 | "norm" should "should not change an already normed string" in { 37 | norm("
") should equal ("
") 38 | } 39 | 40 | it should "norm an empty string to an empty string" in { 41 | norm("") should equal("") 42 | } 43 | 44 | it should "remove leading and trailing space" in { 45 | norm(" a \n") should equal ("A") 46 | } 47 | 48 | it should "remove repeated whitespace" in { 49 | norm(" a\n b ") should equal ("A B") 50 | } 51 | 52 | it should "norm a trailing space before a non-alpha character" in { 53 | norm("a >") should equal ("A>") 54 | } 55 | 56 | it should "norm a crazy div" in { 57 | norm(" < div \n>
\n") should equal ("
") 58 | } 59 | 60 | it should "norm text node correctly" in { 61 | norm("

this is \na text block

") should equal ("

THIS IS A TEXT BLOCK

") 62 | } 63 | 64 | "hyphenate" should "convert camel cased to hyphens" in { 65 | hyphenate("blahHah") should equal ("blah-hah") 66 | } 67 | 68 | it should "work on non camel cased names" in { 69 | hyphenate("blah") should equal ("blah") 70 | } 71 | 72 | it should "remove leading and trailing spaces" in { 73 | hyphenate("blahHah") should equal ("blah-hah") 74 | } 75 | 76 | "processStyleName" should "camel case the name" in { 77 | processStyleName("blahHah") should equal ("blah-hah") 78 | } 79 | 80 | it should "remove leading trailing spaces" in { 81 | processStyleName(" \nblahHah \n\t") should equal ("blah-hah") 82 | } 83 | 84 | it should "process the special mozilla prefix special" in { 85 | processStyleName(" \n msBlahHah") should equal ("-ms-blah-hah") 86 | } 87 | 88 | "quoteValueForStyle" should "return empty string for a boolean" in { 89 | quoteStyleValueForBrowser(None, true) should equal ("") 90 | } 91 | 92 | it should "return empty string for a null value" in { 93 | quoteStyleValueForBrowser(None, null) should equal ("") 94 | } 95 | 96 | it should "return a unitless string if the hint says its unitless" in { 97 | quoteStyleValueForBrowser(Some(StyleHint(values = BitSet(Hints.Unitless))), "200") should equal ("200") 98 | } 99 | 100 | it should "return a string with units if the hint says its not unitless" in { 101 | quoteStyleValueForBrowser(None, "200") should equal ("200px") 102 | } 103 | 104 | "adler32" should "calculate a checksum" in { 105 | val checksum = adler32("
blah is cool
") 106 | //println(s"$checksum") 107 | checksum should be (1628178442) 108 | } 109 | 110 | it should "add it to markup correctly" in { 111 | val markup= addChecksumToMarkup("
blah is cool
") 112 | markup should equal ("""
blah is cool
""") 113 | } 114 | 115 | } 116 | --------------------------------------------------------------------------------