@@ -11,9 +11,20 @@ import { ElementDriver } from './ElementDriver';
1111import HookManager from '../HookManager' ;
1212import { PluginInterface } from './plugins/PluginInterface' ;
1313import BackendResponse from '../BackendResponse' ;
14+ import { ModelBinding } from '../Directive/get_model_binding' ;
1415
1516declare const Turbo : any ;
1617
18+ class ChildComponentWrapper {
19+ component : Component ;
20+ modelBindings : ModelBinding [ ] ;
21+
22+ constructor ( component : Component , modelBindings : ModelBinding [ ] ) {
23+ this . component = component ;
24+ this . modelBindings = modelBindings ;
25+ }
26+ }
27+
1728export default class Component {
1829 readonly element : HTMLElement ;
1930 private readonly backend : BackendInterface ;
@@ -46,7 +57,7 @@ export default class Component {
4657 private nextRequestPromise : Promise < BackendResponse > ;
4758 private nextRequestPromiseResolve : ( response : BackendResponse ) => any ;
4859
49- private children : Map < string , Component > = new Map ( ) ;
60+ private children : Map < string , ChildComponentWrapper > = new Map ( ) ;
5061 private parent : Component | null = null ;
5162
5263 /**
@@ -69,6 +80,8 @@ export default class Component {
6980 this . unsyncedInputsTracker = new UnsyncedInputsTracker ( this , elementDriver ) ;
7081 this . hooks = new HookManager ( ) ;
7182 this . resetPromise ( ) ;
83+
84+ this . onChildComponentModelUpdate = this . onChildComponentModelUpdate . bind ( this ) ;
7285 }
7386
7487 addPlugin ( plugin : PluginInterface ) {
@@ -95,18 +108,22 @@ export default class Component {
95108 * * render:finished (component: Component) => {}
96109 * * loading.state:started (element: HTMLElement, request: BackendRequest) => {}
97110 * * loading.state:finished (element: HTMLElement) => {}
98- * * model:set (model: string, value: any) => {}
111+ * * model:set (model: string, value: any, component: Component ) => {}
99112 */
100113 on ( hookName : string , callback : ( ...args : any [ ] ) => void ) : void {
101114 this . hooks . register ( hookName , callback ) ;
102115 }
103116
117+ off ( hookName : string , callback : ( ...args : any [ ] ) => void ) : void {
118+ this . hooks . unregister ( hookName , callback ) ;
119+ }
120+
104121 set ( model : string , value : any , reRender = false , debounce : number | boolean = false ) : Promise < BackendResponse > {
105122 const promise = this . nextRequestPromise ;
106123 const modelName = normalizeModelName ( model ) ;
107124 const isChanged = this . valueStore . set ( modelName , value ) ;
108125
109- this . hooks . triggerHook ( 'model:set' , model , value ) ;
126+ this . hooks . triggerHook ( 'model:set' , model , value , this ) ;
110127
111128 // the model's data is no longer unsynced
112129 this . unsyncedInputsTracker . markModelAsSynced ( modelName ) ;
@@ -151,13 +168,14 @@ export default class Component {
151168 return this . unsyncedInputsTracker . getModifiedModels ( ) ;
152169 }
153170
154- addChild ( component : Component ) : void {
155- if ( ! component . id ) {
171+ addChild ( child : Component , modelBindings : ModelBinding [ ] = [ ] ) : void {
172+ if ( ! child . id ) {
156173 throw new Error ( 'Children components must have an id.' ) ;
157174 }
158175
159- this . children . set ( component . id , component ) ;
160- component . parent = this ;
176+ this . children . set ( child . id , new ChildComponentWrapper ( child , modelBindings ) ) ;
177+ child . parent = this ;
178+ child . on ( 'model:set' , this . onChildComponentModelUpdate ) ;
161179 }
162180
163181 removeChild ( child : Component ) : void {
@@ -167,16 +185,27 @@ export default class Component {
167185
168186 this . children . delete ( child . id ) ;
169187 child . parent = null ;
188+ child . off ( 'model:set' , this . onChildComponentModelUpdate ) ;
170189 }
171190
172191 getParent ( ) : Component | null {
173192 return this . parent ;
174193 }
175194
176195 getChildren ( ) : Map < string , Component > {
177- return new Map ( this . children ) ;
196+ const children : Map < string , Component > = new Map ( ) ;
197+ this . children . forEach ( ( childComponent , id ) => {
198+ children . set ( id , childComponent . component ) ;
199+ } ) ;
200+
201+ return children ;
178202 }
179203
204+ /**
205+ * Called during morphdom: read props from toEl and re-render if necessary.
206+ *
207+ * @param toEl
208+ */
180209 updateFromNewElement ( toEl : HTMLElement ) : boolean {
181210 const props = this . elementDriver . getComponentProps ( toEl ) ;
182211
@@ -201,6 +230,36 @@ export default class Component {
201230 return false ;
202231 }
203232
233+ /**
234+ * Handles data-model binding from a parent component onto a child.
235+ */
236+ onChildComponentModelUpdate ( modelName : string , value : any , childComponent : Component ) : void {
237+ if ( ! childComponent . id ) {
238+ throw new Error ( 'Missing id' ) ;
239+ }
240+
241+ const childWrapper = this . children . get ( childComponent . id ) ;
242+ if ( ! childWrapper ) {
243+ throw new Error ( 'Missing child' ) ;
244+ }
245+
246+ childWrapper . modelBindings . forEach ( ( modelBinding ) => {
247+ const childModelName = modelBinding . innerModelName || 'value' ;
248+
249+ // skip, unless childModelName matches the model that just changed
250+ if ( childModelName !== modelName ) {
251+ return ;
252+ }
253+
254+ this . set (
255+ modelBinding . modelName ,
256+ value ,
257+ modelBinding . shouldRender ,
258+ modelBinding . debounce
259+ ) ;
260+ } ) ;
261+ }
262+
204263 private tryStartingRequest ( ) : void {
205264 if ( ! this . backendRequest ) {
206265 this . performRequest ( )
@@ -391,7 +450,8 @@ export default class Component {
391450 private getChildrenFingerprints ( ) : any {
392451 const fingerprints : any = { } ;
393452
394- this . children . forEach ( ( child ) => {
453+ this . children . forEach ( ( childComponent ) => {
454+ const child = childComponent . component ;
395455 if ( ! child . id ) {
396456 throw new Error ( 'missing id' ) ;
397457 }
0 commit comments