@@ -9,6 +9,8 @@ import { DataBindProvider } from '@tko/provider.databind'
99import { VirtualProvider } from '@tko/provider.virtual'
1010import { ComponentProvider } from '@tko/provider.component'
1111import { NativeProvider } from '@tko/provider.native'
12+ import { AttributeProvider } from '@tko/provider.attr'
13+ import { TextMustacheProvider , AttributeMustacheProvider } from '@tko/provider.mustache'
1214
1315import { applyBindings , dataFor } from '@tko/bind'
1416
@@ -53,7 +55,15 @@ describe('Components: Component binding', function () {
5355 testNode . innerHTML = '<div data-bind="component: testComponentBindingValue"></div>'
5456
5557 const provider = new MultiProvider ( {
56- providers : [ new ComponentProvider ( ) , new DataBindProvider ( ) , new VirtualProvider ( ) , new NativeProvider ( ) ]
58+ providers : [
59+ new ComponentProvider ( ) ,
60+ new DataBindProvider ( ) ,
61+ new VirtualProvider ( ) ,
62+ new NativeProvider ( ) ,
63+ new AttributeProvider ( ) ,
64+ new TextMustacheProvider ( ) ,
65+ new AttributeMustacheProvider ( )
66+ ]
5767 } )
5868 options . bindingProviderInstance = provider
5969
@@ -93,12 +103,169 @@ describe('Components: Component binding', function () {
93103 } ) . to . throw ( "Unknown component 'test-component'" )
94104 } )
95105
96- it ( 'Throws if the component definition has no template' , function ( ) {
97- components . register ( testComponentName , { } )
106+ it ( 'Uses the element children as template when no template is configured' , function ( ) {
107+ const inner = observable ( 'hello' )
108+ components . register ( 'hello-world' , {
109+ viewModel : function ( ) {
110+ return { greeting : inner }
111+ }
112+ } )
113+ cleanups . push ( ( ) => components . unregister ( 'hello-world' ) )
114+ testNode . innerHTML = '<hello-world><span ko-text="greeting"></span></hello-world>'
115+ applyBindings ( outerViewModel , testNode )
116+ clock . tick ( 1 )
117+ expectContainText ( testNode . children [ 0 ] , 'hello' )
118+ inner ( 'world' )
119+ expectContainText ( testNode . children [ 0 ] , 'world' )
120+ } )
121+
122+ it ( 'Throws if neither a template nor children are provided' , function ( ) {
123+ components . register ( 'hello-world' , {
124+ viewModel : function ( ) {
125+ return { }
126+ }
127+ } )
128+ cleanups . push ( ( ) => components . unregister ( 'hello-world' ) )
129+ testNode . innerHTML = '<hello-world></hello-world>'
98130 expect ( function ( ) {
99131 applyBindings ( outerViewModel , testNode )
100132 clock . tick ( 1 )
101- } ) . to . throw ( "Component 'test-component' has no template" )
133+ } ) . to . throw ( "Component 'hello-world' has no template" )
134+ } )
135+
136+ it ( 'Each instance gets an independent template clone — mutations to one do not bleed into siblings or subsequent instances' , function ( ) {
137+ components . register ( 'hello-world' , {
138+ template : '<p class="pristine">hello</p>'
139+ } )
140+ cleanups . push ( ( ) => components . unregister ( 'hello-world' ) )
141+
142+ testNode . innerHTML = '<hello-world></hello-world><hello-world></hello-world>'
143+ applyBindings ( outerViewModel , testNode )
144+ clock . tick ( 1 )
145+
146+ const [ firstHost , secondHost ] = Array . from ( testNode . children )
147+ const first = firstHost . querySelector ( 'p' ) !
148+ const second = secondHost . querySelector ( 'p' ) !
149+
150+ first . className = 'mutated'
151+ first . textContent = 'MUTATED'
152+ expect ( second . className ) . to . equal ( 'pristine' )
153+ expect ( second . textContent ) . to . equal ( 'hello' )
154+
155+ const thirdHost = document . createElement ( 'hello-world' )
156+ testNode . appendChild ( thirdHost )
157+ applyBindings ( outerViewModel , thirdHost )
158+ clock . tick ( 1 )
159+
160+ const third = thirdHost . querySelector ( 'p' ) !
161+ expect ( third . className ) . to . equal ( 'pristine' )
162+ expect ( third . textContent ) . to . equal ( 'hello' )
163+ } )
164+
165+ it ( 'Uses children as template with mixed text and {{ }} mustache interpolation' , function ( ) {
166+ const name = observable ( 'world' )
167+ components . register ( 'hello-world' , {
168+ viewModel : function ( ) {
169+ return { name }
170+ }
171+ } )
172+ cleanups . push ( ( ) => components . unregister ( 'hello-world' ) )
173+ testNode . innerHTML = '<hello-world>Hello, <strong>{{ name }}</strong>!</hello-world>'
174+ applyBindings ( outerViewModel , testNode )
175+ clock . tick ( 1 )
176+ expectContainText ( testNode . children [ 0 ] , 'Hello, world!' )
177+ name ( 'TKO' )
178+ expectContainText ( testNode . children [ 0 ] , 'Hello, TKO!' )
179+ } )
180+
181+ it ( 'Configured template wins — inline children are discarded' , function ( ) {
182+ components . register ( 'hello-world' , {
183+ template : '<p class="from-config">from template config</p>'
184+ } )
185+ cleanups . push ( ( ) => components . unregister ( 'hello-world' ) )
186+ testNode . innerHTML = '<hello-world><p class="from-children">from inline</p></hello-world>'
187+ applyBindings ( outerViewModel , testNode )
188+ clock . tick ( 1 )
189+ expectContainText ( testNode . children [ 0 ] , 'from template config' )
190+ expect ( testNode . children [ 0 ] . querySelector ( '.from-children' ) ) . to . equal ( null )
191+ } )
192+
193+ it ( 'Exposes $component, $data, and $parent inside children-as-template' , function ( ) {
194+ const parentFlag = observable ( 'parent' )
195+ components . register ( 'hello-world' , {
196+ viewModel : function ( ) {
197+ return { greeting : 'ello' }
198+ }
199+ } )
200+ cleanups . push ( ( ) => components . unregister ( 'hello-world' ) )
201+ testNode . innerHTML =
202+ '<hello-world>' +
203+ '<span class="component" data-bind="text: $component.greeting"></span>' +
204+ '<span class="data" data-bind="text: $data.greeting"></span>' +
205+ '<span class="parent" data-bind="text: $parent.parentFlag"></span>' +
206+ '</hello-world>'
207+ applyBindings ( { ...outerViewModel , parentFlag } , testNode )
208+ clock . tick ( 1 )
209+ expect ( testNode . children [ 0 ] . querySelector ( '.component' ) ! . textContent ) . to . equal ( 'ello' )
210+ expect ( testNode . children [ 0 ] . querySelector ( '.data' ) ! . textContent ) . to . equal ( 'ello' )
211+ expect ( testNode . children [ 0 ] . querySelector ( '.parent' ) ! . textContent ) . to . equal ( 'parent' )
212+ } )
213+
214+ it ( 'Nested components both use children-as-template' , function ( ) {
215+ components . register ( 'outer-comp' , {
216+ viewModel : function ( ) {
217+ return { outerMsg : 'outer' }
218+ }
219+ } )
220+ components . register ( 'inner-comp' , {
221+ viewModel : function ( ) {
222+ return { innerMsg : 'inner' }
223+ }
224+ } )
225+ cleanups . push ( ( ) => {
226+ components . unregister ( 'outer-comp' )
227+ components . unregister ( 'inner-comp' )
228+ } )
229+ testNode . innerHTML =
230+ '<outer-comp>' +
231+ '<span class="outer" ko-text="outerMsg"></span>' +
232+ '<inner-comp><span class="inner" ko-text="innerMsg"></span></inner-comp>' +
233+ '</outer-comp>'
234+ applyBindings ( outerViewModel , testNode )
235+ clock . tick ( 1 )
236+ expect ( testNode . children [ 0 ] . querySelector ( '.outer' ) ! . textContent ) . to . equal ( 'outer' )
237+ expect ( testNode . children [ 0 ] . querySelector ( '.inner' ) ! . textContent ) . to . equal ( 'inner' )
238+ } )
239+
240+ it ( 'Rebuild via observable component name restores original children for the new component' , function ( ) {
241+ class CompB {
242+ msg = 'from-children'
243+ }
244+ components . register ( 'comp-a' , { template : '<span class="from-a">A</span>' } )
245+ components . register ( 'comp-b' , { viewModel : CompB } )
246+ cleanups . push ( ( ) => {
247+ components . unregister ( 'comp-a' )
248+ components . unregister ( 'comp-b' )
249+ } )
250+
251+ const compName = observable ( 'comp-a' )
252+ testNode . innerHTML =
253+ '<div data-bind="component: { name: compName }">' +
254+ '<span class="from-children" data-bind="text: msg"></span>' +
255+ '</div>'
256+ applyBindings ( { compName } , testNode )
257+ clock . tick ( 1 )
258+
259+ expect ( testNode . children [ 0 ] . querySelector ( '.from-a' ) ) . to . not . equal ( null )
260+ expect ( testNode . children [ 0 ] . querySelector ( '.from-children' ) ) . to . equal ( null )
261+
262+ compName ( 'comp-b' )
263+ clock . tick ( 1 )
264+
265+ expect ( testNode . children [ 0 ] . querySelector ( '.from-a' ) ) . to . equal ( null )
266+ const rendered = testNode . children [ 0 ] . querySelector ( '.from-children' )
267+ expect ( rendered ) . to . not . equal ( null )
268+ expect ( rendered ! . textContent ) . to . equal ( 'from-children' )
102269 } )
103270
104271 it ( 'Controls descendant bindings' , function ( ) {
0 commit comments