[{"data":1,"prerenderedAt":805},["ShallowReactive",2],{"\u002Fnext\u002Fdodock\u002Fdeveloppeurs\u002Fcontroleurs-de-formulaire":3,"section:\u002Fnext\u002Fdodock\u002Fdeveloppeurs\u002Fcontroleurs-de-formulaire":804},{"id":4,"title":5,"body":6,"description":791,"extension":792,"meta":793,"navigation":794,"path":800,"seo":801,"stem":802,"__hash__":803},"docs\u002F2.next\u002F3.dodock\u002F10.developpeurs\u002Fcontroleurs-de-formulaire.md","Contrôleurs de formulaire",{"type":7,"value":8,"toc":772},"minimark",[9,16,21,41,44,48,53,56,129,132,170,174,192,199,203,207,229,233,237,270,296,300,347,360,364,395,399,402,440,448,452,455,539,548,552,562,583,586,590,596,601,645,650,692,695,712,716,756,760,768],[10,11,12],"dokos",{},[13,14,15],"p",{},"Disponible depuis la version 5.x de Dodock",[17,18,20],"h2",{"id":19},"introduction","Introduction",[13,22,23,24,28,29,33,34,33,37,40],{},"Un ",[25,26,27],"strong",{},"contrôleur de formulaire"," est une classe JavaScript qui définit le comportement d'un type de document lors de la saisie et de l'affichage. Il s'agit de l'équivalent moderne et structuré des anciens scripts clients (Client Scripts), regroupant les gestionnaires d'événements (",[30,31,32],"code",{},"onload",", ",[30,35,36],{},"validate",[30,38,39],{},"refresh",", etc.) dans une classe réutilisable et extensible.",[13,42,43],{},"Les applications comme Dokos ou ERPNext lient traditionnellement ces contrôleurs en haut d'un script de formulaire. Dodock introduit désormais une API dédiée pour les enregistrer proprement et s'assurer qu'ils sont bien chargés au bon moment dans le cycle de vie du formulaire.",[17,45,47],{"id":46},"pourquoi-un-nouveau-mécanisme","Pourquoi un nouveau mécanisme",[49,50,52],"h3",{"id":51},"les-limites-de-lapproche-historique","Les limites de l'approche historique",[13,54,55],{},"Avant cette évolution, les applications liaient un contrôleur de classe au formulaire en l'étendant en haut du script de type de document :",[57,58,63],"pre",{"className":59,"code":60,"language":61,"meta":62,"style":62},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","extend_cscript(cur_frm.cscript, new erpnext.stock.DeliveryNoteController({ frm: cur_frm }));\n","js","",[30,64,65],{"__ignoreMap":62},[66,67,70,74,78,82,85,88,91,94,96,99,101,104,107,110,114,117,120,123,126],"span",{"class":68,"line":69},"line",1,[66,71,73],{"class":72},"s2Zo4","extend_cscript",[66,75,77],{"class":76},"sTEyZ","(cur_frm",[66,79,81],{"class":80},"sMK4o",".",[66,83,84],{"class":76},"cscript",[66,86,87],{"class":80},",",[66,89,90],{"class":80}," new",[66,92,93],{"class":76}," erpnext",[66,95,81],{"class":80},[66,97,98],{"class":76},"stock",[66,100,81],{"class":80},[66,102,103],{"class":72},"DeliveryNoteController",[66,105,106],{"class":76},"(",[66,108,109],{"class":80},"{",[66,111,113],{"class":112},"swJcz"," frm",[66,115,116],{"class":80},":",[66,118,119],{"class":76}," cur_frm ",[66,121,122],{"class":80},"}",[66,124,125],{"class":76},"))",[66,127,128],{"class":80},";\n",[13,130,131],{},"Cette approche présentait deux inconvénients majeurs :",[133,134,135,152],"ul",{},[136,137,138,144,145,147,148,151],"li",{},[25,139,140,141],{},"Pas de portée ",[30,142,143],{},"frm"," : le script s'exécute sans objet ",[30,146,143],{}," dans la portée, ",[30,149,150],{},"cur_frm"," est donc le seul moyen d'y accéder.",[136,153,154,157,158,161,162,165,166,169],{},[25,155,156],{},"Gestionnaires d'événements ignorés"," : le branchement dans un gestionnaire ",[30,159,160],{},"setup"," ne fonctionne pas, car l'événement ",[30,163,164],{},"trigger(\"setup\")"," collecte les gestionnaires à l'ancienne avant que ceux de la classe ne puissent être enregistrés. La méthode ",[30,167,168],{},"setup()"," du contrôleur n'est donc jamais déclenchée.",[49,171,173],{"id":172},"une-api-explicite-et-fiable","Une API explicite et fiable",[13,175,176,177,180,181,184,185,188,189,191],{},"La fonction ",[30,178,179],{},"frappe.ui.form.set_controller(doctype, ControllerClass)"," résout ces problèmes en enregistrant la classe auprès du gestionnaire de scripts. À l'initialisation du formulaire, après l'exécution des scripts de formulaire et des scripts clients, le ",[30,182,183],{},"ScriptManager"," branche automatiquement le contrôleur pour le type de document concerné, ",[25,186,187],{},"avant"," que l'événement ",[30,190,160],{}," ne soit déclenché.",[13,193,194,195,198],{},"Si un script a déjà lié une sous-classe du contrôleur — par exemple pour étendre un contrôleur d'application depuis un ",[30,196,197],{},"doctype_js"," personnalisé — ce branchement est conservé et prioritaire.",[200,201,202],"mermaid",{},"\nflowchart TD\n    A[Ouverture du formulaire] --> B[Chargement du script de formulaire]\n    B --> C[Exécution des scripts clients]\n    C --> D[ScriptManager branche le contrôleur enregistré]\n    D --> E{Un script a-t-il déjà lié une sous-classe ?}\n    E -- Oui --> F[Conserver le branchement existant]\n    E -- Non --> G[Lier la classe enregistrée]\n    F --> H[Déclenchement de l'événement setup]\n    G --> H\n    H --> I[Formulaire prêt à la saisie]\n",[17,204,206],{"id":205},"prérequis","Prérequis",[133,208,209,223],{},[136,210,211,212,215,216,218,219,222],{},"Une application avec un script de formulaire (",[30,213,214],{},"\u003Cdoctype>.js"," ou ",[30,217,197],{}," dans ",[30,220,221],{},"hooks.py",").",[136,224,225,226,81],{},"Une classe de contrôleur dont le constructeur accepte ",[30,227,228],{},"{ frm }",[17,230,232],{"id":231},"enregistrer-un-contrôleur","Enregistrer un contrôleur",[49,234,236],{"id":235},"signature","Signature",[57,238,240],{"className":59,"code":239,"language":61,"meta":62,"style":62},"frappe.ui.form.set_controller(doctype, ControllerClass)\n",[30,241,242],{"__ignoreMap":62},[66,243,244,247,249,252,254,257,259,262,265,267],{"class":68,"line":69},[66,245,246],{"class":76},"frappe",[66,248,81],{"class":80},[66,250,251],{"class":76},"ui",[66,253,81],{"class":80},[66,255,256],{"class":76},"form",[66,258,81],{"class":80},[66,260,261],{"class":72},"set_controller",[66,263,264],{"class":76},"(doctype",[66,266,87],{"class":80},[66,268,269],{"class":76}," ControllerClass)\n",[133,271,272,287],{},[136,273,274,277,278,282,283,286],{},[30,275,276],{},"doctype"," ",[279,280,281],"em",{},"(chaîne)"," — le nom interne (anglais) du type de document, par exemple ",[30,284,285],{},"\"Delivery Note\"",". Le libellé traduit ne fonctionne pas.",[136,288,289,277,292,295],{},[30,290,291],{},"ControllerClass",[279,293,294],{},"(classe)"," — la classe de contrôleur à lier à ce type de document.",[49,297,299],{"id":298},"exemple","Exemple",[57,301,303],{"className":59,"code":302,"language":61,"meta":62,"style":62},"frappe.ui.form.set_controller(\"Delivery Note\", erpnext.stock.DeliveryNoteController);\n",[30,304,305],{"__ignoreMap":62},[66,306,307,309,311,313,315,317,319,321,323,326,330,332,334,336,338,340,342,345],{"class":68,"line":69},[66,308,246],{"class":76},[66,310,81],{"class":80},[66,312,251],{"class":76},[66,314,81],{"class":80},[66,316,256],{"class":76},[66,318,81],{"class":80},[66,320,261],{"class":72},[66,322,106],{"class":76},[66,324,325],{"class":80},"\"",[66,327,329],{"class":328},"sfazB","Delivery Note",[66,331,325],{"class":80},[66,333,87],{"class":80},[66,335,93],{"class":76},[66,337,81],{"class":80},[66,339,98],{"class":76},[66,341,81],{"class":80},[66,343,344],{"class":76},"DeliveryNoteController)",[66,346,128],{"class":80},[13,348,349,350,353,354,356,357,359],{},"ERPNext utilise cette instruction à la fin de ",[30,351,352],{},"erpnext\u002Fstock\u002Fdoctype\u002Fdelivery_note\u002Fdelivery_note.js",". Elle enregistre ",[30,355,103],{}," comme contrôleur du type de document ",[30,358,329],{},". Le gestionnaire de scripts se chargera de l'instancier et de le lier au formulaire au moment opportun.",[17,361,363],{"id":362},"fonctionnement-interne","Fonctionnement interne",[13,365,366,368,369,372,373,376,377,380,381,384,385,387,388,390,391,394],{},[30,367,261],{}," mémorise la classe dans ",[30,370,371],{},"frappe.ui.form.controllers[doctype]",". La méthode ",[30,374,375],{},"ScriptManager.bind_controller()"," l'instancie avec ",[30,378,379],{},"new ControllerClass({ frm })"," et la fusionne dans ",[30,382,383],{},"frm.cscript"," par ",[30,386,73],{},". Elle ne fait rien si ",[30,389,383],{}," est déjà une instance de la classe (test ",[30,392,393],{},"instanceof","), donc une sous-classe déjà liée est conservée.",[17,396,398],{"id":397},"cycle-de-vie-du-branchement","Cycle de vie du branchement",[13,400,401],{},"L'ordre précis d'exécution garantit que les contrôleurs sont en place avant que les événements ne se déclenchent :",[403,404,405,411,417,426],"ol",{},[136,406,407,410],{},[25,408,409],{},"Scripts de formulaire"," — les scripts déclarés sur le type de document s'exécutent.",[136,412,413,416],{},[25,414,415],{},"Scripts clients"," — les scripts clients configurés via l'interface s'appliquent.",[136,418,419,422,423,425],{},[25,420,421],{},"Branchement du contrôleur"," — le ",[30,424,183],{}," instancie et branche le contrôleur enregistré pour le type de document, à moins qu'une sous-classe n'ait déjà été liée par un script.",[136,427,428,433,434,436,437,439],{},[25,429,430,431],{},"Événement ",[30,432,160],{}," — l'événement ",[30,435,160],{}," est déclenché sur le formulaire. Les méthodes ",[30,438,168],{}," du contrôleur sont désormais appelées.",[13,441,442,443,384,445,447],{},"Cette séquence permet aux développeurs de remplacer l'usage de ",[30,444,150],{},[30,446,143],{}," dans la portée des méthodes du contrôleur, et garantit que les méthodes héritées fonctionnent comme prévu.",[17,449,451],{"id":450},"héritage-et-extensions","Héritage et extensions",[13,453,454],{},"Le mécanisme respecte l'héritage des contrôleurs. Si une application ou un script personnalisé étend un contrôleur existant :",[57,456,458],{"className":59,"code":457,"language":61,"meta":62,"style":62},"class MonBonDeLivraisonPersonnalise extends erpnext.stock.DeliveryNoteController {\n  refresh(frm) {\n    super.refresh(frm);\n    \u002F\u002F Comportement supplémentaire\n  }\n}\n",[30,459,460,486,502,520,527,533],{"__ignoreMap":62},[66,461,462,466,470,473,475,477,479,481,483],{"class":68,"line":69},[66,463,465],{"class":464},"spNyl","class",[66,467,469],{"class":468},"sBMFI"," MonBonDeLivraisonPersonnalise",[66,471,472],{"class":464}," extends",[66,474,93],{"class":468},[66,476,81],{"class":80},[66,478,98],{"class":468},[66,480,81],{"class":80},[66,482,103],{"class":468},[66,484,485],{"class":80}," {\n",[66,487,489,492,494,497,500],{"class":68,"line":488},2,[66,490,491],{"class":112},"  refresh",[66,493,106],{"class":80},[66,495,143],{"class":496},"sHdIc",[66,498,499],{"class":80},")",[66,501,485],{"class":80},[66,503,505,508,510,512,514,516,518],{"class":68,"line":504},3,[66,506,507],{"class":76},"    super",[66,509,81],{"class":80},[66,511,39],{"class":72},[66,513,106],{"class":112},[66,515,143],{"class":76},[66,517,499],{"class":112},[66,519,128],{"class":80},[66,521,523],{"class":68,"line":522},4,[66,524,526],{"class":525},"sHwdD","    \u002F\u002F Comportement supplémentaire\n",[66,528,530],{"class":68,"line":529},5,[66,531,532],{"class":80},"  }\n",[66,534,536],{"class":68,"line":535},6,[66,537,538],{"class":80},"}\n",[13,540,541,542,544,545,547],{},"Et le lie explicitement avant que le ",[30,543,183],{}," ne s'exécute, le branchement personnalisé est conservé. Le contrôleur de base enregistré via ",[30,546,261],{}," ne remplace pas cette sous-classe.",[17,549,551],{"id":550},"types-de-documents-concernés","Types de documents concernés",[13,553,554,555,557,558,561],{},"Dodock utilise ",[30,556,261],{}," pour lier le contrôleur ",[30,559,560],{},"frappe.model.DocTypeController"," sur deux types de documents techniques :",[133,563,564,574],{},[136,565,566,569,570,573],{},[25,567,568],{},"Type de document"," (",[30,571,572],{},"DocType",") — le formulaire de configuration des types de documents.",[136,575,576,569,579,582],{},[25,577,578],{},"Formulaire personnalisé",[30,580,581],{},"Customize Form",") — l'interface de personnalisation des formulaires.",[13,584,585],{},"Ces branchements internes illustrent l'adoption de l'API au cœur du framework et servent de référence pour les applications tierces.",[17,587,589],{"id":588},"migration-depuis-lancienne-approche","Migration depuis l'ancienne approche",[13,591,592,593,595],{},"Si vous maintenez une application ou un script personnalisé qui utilise ",[30,594,73],{}," pour lier un contrôleur, vous pouvez migrer progressivement :",[13,597,598],{},[25,599,600],{},"Avant :",[57,602,603],{"className":59,"code":60,"language":61,"meta":62,"style":62},[30,604,605],{"__ignoreMap":62},[66,606,607,609,611,613,615,617,619,621,623,625,627,629,631,633,635,637,639,641,643],{"class":68,"line":69},[66,608,73],{"class":72},[66,610,77],{"class":76},[66,612,81],{"class":80},[66,614,84],{"class":76},[66,616,87],{"class":80},[66,618,90],{"class":80},[66,620,93],{"class":76},[66,622,81],{"class":80},[66,624,98],{"class":76},[66,626,81],{"class":80},[66,628,103],{"class":72},[66,630,106],{"class":76},[66,632,109],{"class":80},[66,634,113],{"class":112},[66,636,116],{"class":80},[66,638,119],{"class":76},[66,640,122],{"class":80},[66,642,125],{"class":76},[66,644,128],{"class":80},[13,646,647],{},[25,648,649],{},"Après :",[57,651,652],{"className":59,"code":302,"language":61,"meta":62,"style":62},[30,653,654],{"__ignoreMap":62},[66,655,656,658,660,662,664,666,668,670,672,674,676,678,680,682,684,686,688,690],{"class":68,"line":69},[66,657,246],{"class":76},[66,659,81],{"class":80},[66,661,251],{"class":76},[66,663,81],{"class":80},[66,665,256],{"class":76},[66,667,81],{"class":80},[66,669,261],{"class":72},[66,671,106],{"class":76},[66,673,325],{"class":80},[66,675,329],{"class":328},[66,677,325],{"class":80},[66,679,87],{"class":80},[66,681,93],{"class":76},[66,683,81],{"class":80},[66,685,98],{"class":76},[66,687,81],{"class":80},[66,689,344],{"class":76},[66,691,128],{"class":80},[13,693,694],{},"La migration vous permet de :",[133,696,697,703,709],{},[136,698,699,700,702],{},"Supprimer la dépendance à ",[30,701,150],{}," dans le branchement.",[136,704,705,706,708],{},"Garantir que la méthode ",[30,707,168],{}," du contrôleur est bien déclenchée.",[136,710,711],{},"Simplifier la lecture et la maintenance du code d'extension.",[17,713,715],{"id":714},"bonnes-pratiques","Bonnes pratiques",[133,717,718,727,736,750],{},[136,719,720,723,724,726],{},[25,721,722],{},"Un seul contrôleur par type de document"," — n'enregistrez qu'une seule classe par ",[30,725,276],{},". Pour étendre un contrôleur, créez une sous-classe et branchez-la explicitement si nécessaire.",[136,728,729,732,733,735],{},[25,730,731],{},"Placez l'enregistrement au plus haut niveau"," — déclarez ",[30,734,261],{}," dans le script de formulaire de l'application, pas dans un gestionnaire d'événements.",[136,737,738,743,744,746,747,749],{},[25,739,740,741],{},"Ne référencez pas ",[30,742,150],{}," — utilisez l'argument ",[30,745,143],{}," passé aux méthodes du contrôleur. L'API est conçue pour rendre ",[30,748,150],{}," superflu.",[136,751,752,755],{},[25,753,754],{},"Testez les sous-classes"," — si votre application étend un contrôleur d'une autre application, vérifiez que le branchement de votre sous-classe survit bien au branchement automatique du contrôleur de base.",[17,757,759],{"id":758},"voir-aussi","Voir aussi",[133,761,762],{},[136,763,764],{},[765,766,415],"a",{"href":767},"\u002Fnext\u002Fdodock\u002Ffonctionnalites\u002Fpersonnalisations\u002Fclient_script",[769,770,771],"style",{},"html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}",{"title":62,"searchDepth":69,"depth":488,"links":773},[774,775,779,780,784,785,786,787,788,789,790],{"id":19,"depth":488,"text":20},{"id":46,"depth":488,"text":47,"children":776},[777,778],{"id":51,"depth":504,"text":52},{"id":172,"depth":504,"text":173},{"id":205,"depth":488,"text":206},{"id":231,"depth":488,"text":232,"children":781},[782,783],{"id":235,"depth":504,"text":236},{"id":298,"depth":504,"text":299},{"id":362,"depth":488,"text":363},{"id":397,"depth":488,"text":398},{"id":450,"depth":488,"text":451},{"id":550,"depth":488,"text":551},{"id":588,"depth":488,"text":589},{"id":714,"depth":488,"text":715},{"id":758,"depth":488,"text":759},"Découvrez comment enregistrer et lier des classes de contrôleur de formulaire à des types de documents spécifiques avec frappe.ui.form.set_controller.","md",{"reviewed":794,"last_verified":795,"sources":796},true,"2026-11-03",[797,798,799],"dodock@develop:frappe\u002Fpublic\u002Fjs\u002Ffrappe\u002Fform\u002Fscript_manager.js","dodock@develop:frappe\u002Fcore\u002Fdoctype\u002Fdoctype\u002Fdoctype.js","dodock@develop:frappe\u002Fcustom\u002Fdoctype\u002Fcustomize_form\u002Fcustomize_form.js","\u002Fnext\u002Fdodock\u002Fdeveloppeurs\u002Fcontroleurs-de-formulaire",{"title":5,"description":791},"2.next\u002F3.dodock\u002F10.developpeurs\u002Fcontroleurs-de-formulaire","HILxwG3bMu3VPJM0zBUeA02mdMZMIEqCNDIAWL4KYGY",{},1793871743045]