|
| 1 | +# cf-ui Django Migration Guide |
| 2 | + |
| 3 | +Load this prompt at the start of any session where you will be migrating a Django project to use `cf-ui` components. It gives you the full picture before you touch any files. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## What cf-ui is |
| 8 | + |
| 9 | +`cf-ui` (`component-framework-ui`) is the official UI kit for `component-framework`. It ships Bulma component templates as django-cotton components, so your templates use `<c-cf.card>`, `<c-cf.notification>`, etc. instead of hand-rolled Bulma HTML. |
| 10 | + |
| 11 | +Package: `pip install "cf-ui[bulma]" "git+https://github.com/fsecada01/component-framework.git"` |
| 12 | + |
| 13 | +Docs / source: https://github.com/fsecada01/component-framework-ui |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Critical gotchas — read before touching any file |
| 18 | + |
| 19 | +1. **`<c-vars>` not `<c-props>`** — django-cotton 2.x renamed the tag. RankedJobs already uses `<c-vars>` correctly. cf-ui also uses `<c-vars>`. No action needed. |
| 20 | + |
| 21 | +2. **`extra_class` not `class`** — all cf-ui components use `extra_class` for the root element. `class` is a Python reserved word in JinjaX `{#def}` headers and was renamed across the board. |
| 22 | + |
| 23 | +3. **Inner element class props** — form components also accept: |
| 24 | + - `input_class` → appended to the `<input>`, `<select>`, or `<textarea>` element |
| 25 | + - `control_class` → appended to the `<div class="control">` in `CfCheckboxGroup` |
| 26 | + |
| 27 | +4. **Do NOT call `{% cf_ui_head %}`** in RankedJobs — the project compiles Bulma from source SCSS with brand overrides (`--rj-primary`, custom fonts, etc.). The CDN Bulma link from cf-ui would load a second, conflicting stylesheet. Only call `{% cf_ui_body %}` to load Alpine + `cf_ui_alpine.js`. |
| 28 | + |
| 29 | +5. **`"libraries"` key required in TEMPLATES** — the `cf_ui.django` app name prevents Django's templatetag autodiscovery. Add to `TEMPLATES[0]["OPTIONS"]`: |
| 30 | + ```python |
| 31 | + "libraries": {"cf_ui": "cf_ui.templatetags.cf_ui"}, |
| 32 | + ``` |
| 33 | + |
| 34 | +6. **`component_framework` is already in INSTALLED_APPS** — verify it satisfies `>=0.4` before installing cf-ui. Run `pip show component-framework`. |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +## Settings changes |
| 39 | + |
| 40 | +```python |
| 41 | +# settings/base.py |
| 42 | + |
| 43 | +INSTALLED_APPS = [ |
| 44 | + ... |
| 45 | + "cf_ui.django.CfUiConfig", # add this |
| 46 | +] |
| 47 | + |
| 48 | +CF_UI_THEME = "bulma" # add this |
| 49 | + |
| 50 | +TEMPLATES = [{ |
| 51 | + ... |
| 52 | + "OPTIONS": { |
| 53 | + ... |
| 54 | + "libraries": {"cf_ui": "cf_ui.templatetags.cf_ui"}, # add this key |
| 55 | + }, |
| 56 | +}] |
| 57 | +``` |
| 58 | + |
| 59 | +--- |
| 60 | + |
| 61 | +## Asset loading |
| 62 | + |
| 63 | +RankedJobs loads its own compiled Bulma CSS. **Don't use `{% cf_ui_head %}`.** |
| 64 | + |
| 65 | +Replace the vanilla JS navbar burger toggle in `cotton/layouts/base.html` with Alpine. Add `{% cf_ui_body %}` before `</body>`: |
| 66 | + |
| 67 | +**Before** (`cotton/layouts/base.html`): |
| 68 | +```html |
| 69 | +{% include 'snippets/js.html' %} |
| 70 | +<script> |
| 71 | + document.addEventListener("DOMContentLoaded", () => { |
| 72 | + document.querySelectorAll(".navbar-burger").forEach((burger) => { |
| 73 | + burger.addEventListener("click", () => { |
| 74 | + const target = document.getElementById(burger.dataset.target); |
| 75 | + burger.classList.toggle("is-active"); |
| 76 | + if (target) target.classList.toggle("is-active"); |
| 77 | + }); |
| 78 | + }); |
| 79 | + }); |
| 80 | +</script> |
| 81 | +``` |
| 82 | + |
| 83 | +**After**: |
| 84 | +```html |
| 85 | +{% load cf_ui %} |
| 86 | +{% include 'snippets/js.html' %} |
| 87 | +{% cf_ui_body %} {# loads cf_ui_alpine.js + Alpine CDN #} |
| 88 | +``` |
| 89 | + |
| 90 | +Then update the navbar burger span to use Alpine (see Navbar section below). |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## Component migration reference |
| 95 | + |
| 96 | +### Notifications / Messages |
| 97 | + |
| 98 | +**`snippets/message.html`** — Django messages framework: |
| 99 | + |
| 100 | +Before: |
| 101 | +```html |
| 102 | +<article class="message is-small is-info" style="max-width: 640px; margin: 1rem auto"> |
| 103 | + <div class="message-header"> |
| 104 | + <p>{{ message.tags|title }}</p> |
| 105 | + <button class="delete" aria-label="delete"></button> |
| 106 | + </div> |
| 107 | + <div class="message-body"> |
| 108 | + {{ message|safe }} |
| 109 | + </div> |
| 110 | +</article> |
| 111 | +``` |
| 112 | + |
| 113 | +After: |
| 114 | +```html |
| 115 | +{% load cf_ui %} |
| 116 | +<c-cf.notification message="{{ message|safe }}" type="{{ message.tags }}" dismissible="true" |
| 117 | + extra_class="mt-3" style="max-width: 640px; margin: 1rem auto" /> |
| 118 | +``` |
| 119 | + |
| 120 | +**Inline error notifications** (used throughout components, e.g. `gap_analysis.html`): |
| 121 | + |
| 122 | +Before: |
| 123 | +```html |
| 124 | +<div class="notification is-danger is-light py-2 px-3 is-size-7"> |
| 125 | + {{ state.error }} |
| 126 | +</div> |
| 127 | +``` |
| 128 | + |
| 129 | +After: |
| 130 | +```html |
| 131 | +<c-cf.notification message="{{ state.error }}" type="danger" |
| 132 | + dismissible="false" extra_class="py-2 px-3 is-size-7" /> |
| 133 | +``` |
| 134 | + |
| 135 | +**Inline info notifications**: |
| 136 | + |
| 137 | +Before: |
| 138 | +```html |
| 139 | +<div class="notification is-info is-light py-2 px-3 is-size-7"> |
| 140 | + Upload a resume to see your fit score for this role. |
| 141 | +</div> |
| 142 | +``` |
| 143 | + |
| 144 | +After: |
| 145 | +```html |
| 146 | +<c-cf.notification message="Upload a resume to see your fit score for this role." |
| 147 | + type="info" dismissible="false" extra_class="py-2 px-3 is-size-7" /> |
| 148 | +``` |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +### Progress bar |
| 153 | + |
| 154 | +Used in `gap_analysis.html` for the resume fit score. cf-ui's `CfProgress` wraps the Bulma `<progress>` element. |
| 155 | + |
| 156 | +Before: |
| 157 | +```html |
| 158 | +<progress class="progress is-small |
| 159 | + {% if state.match_pct >= 0.7 %}is-success{% elif state.match_pct >= 0.4 %}is-warning{% else %}is-danger{% endif %}" |
| 160 | + value="{{ state.match_pct|floatformat:2 }}" |
| 161 | + max="1">{{ state.match_pct }}</progress> |
| 162 | +``` |
| 163 | + |
| 164 | +After: |
| 165 | +```html |
| 166 | +{% with pct=state.match_pct %} |
| 167 | +{% if pct >= 0.7 %}{% with bar_type="success" %}{% elif pct >= 0.4 %}{% with bar_type="warning" %}{% else %}{% with bar_type="danger" %}{% endif %} |
| 168 | +<c-cf.progress value="{{ pct|floatformat:2 }}" max="1" type="{{ bar_type }}" |
| 169 | + extra_class="is-small" /> |
| 170 | +{% endwith %}{% endwith %}{% endwith %} |
| 171 | +{% endwith %} |
| 172 | +``` |
| 173 | + |
| 174 | +> **Note:** The conditional type logic is awkward in Django templates. If this component lives in Python (e.g. a `component-framework` Component class), drive `bar_type` from `self.state` and pass it in directly. That's the cleaner path. |
| 175 | +
|
| 176 | +--- |
| 177 | + |
| 178 | +### Modal |
| 179 | + |
| 180 | +`Job_Detail_Modal.html` uses legacy Semantic UI markup and is already broken. Replace it with cf-ui's modal. |
| 181 | + |
| 182 | +Before: |
| 183 | +```html |
| 184 | +<div class="ui modal" id="{{ job.id }}" role="dialog"> |
| 185 | + <span class="close"><i class="close icon"></i></span> |
| 186 | + <div class="header">Emailable Job Post!</div> |
| 187 | + <div class="scrolling content"> |
| 188 | + <div class="ui header">{{ job.job_title }}</div> |
| 189 | + {{ job.jobdesc|escape|linebreaks }} |
| 190 | + </div> |
| 191 | + <div class="actions"> |
| 192 | + <div class="ui black deny button">Nope</div> |
| 193 | + <div class="ui positive right labeled icon button">Let's apply!!</div> |
| 194 | + </div> |
| 195 | +</div> |
| 196 | +``` |
| 197 | + |
| 198 | +After: |
| 199 | +```html |
| 200 | +<c-cf.modal id="job-{{ job.id }}"> |
| 201 | + <c-slot name="header">{{ job.job_title }}</c-slot> |
| 202 | + {{ job.jobdesc|escape|linebreaks }} |
| 203 | + <c-slot name="footer"> |
| 204 | + <button class="button" @click="close()">Close</button> |
| 205 | + <a class="button is-primary" href="{{ job.apply_url }}" target="_blank">Apply</a> |
| 206 | + </c-slot> |
| 207 | +</c-cf.modal> |
| 208 | + |
| 209 | +{# Trigger button — dispatches custom event to the modal by id #} |
| 210 | +<button class="button is-small" |
| 211 | + @click="Alpine.store('cf').modal.open('job-{{ job.id }}')"> |
| 212 | + View Details |
| 213 | +</button> |
| 214 | +``` |
| 215 | + |
| 216 | +> **Requires Alpine** — add `{% cf_ui_body %}` to the base layout first (see Asset Loading section). |
| 217 | +
|
| 218 | +--- |
| 219 | + |
| 220 | +### Table pagination |
| 221 | + |
| 222 | +Any paginated view can use `<c-cf.pagination>` instead of hand-rolled pagination markup. |
| 223 | + |
| 224 | +Before (typical Django paginator markup): |
| 225 | +```html |
| 226 | +{% if page_obj.has_other_pages %} |
| 227 | +<nav class="pagination is-centered" role="navigation"> |
| 228 | + {% if page_obj.has_previous %} |
| 229 | + <a class="pagination-previous" href="?page={{ page_obj.previous_page_number }}">Previous</a> |
| 230 | + {% else %} |
| 231 | + <a class="pagination-previous" disabled>Previous</a> |
| 232 | + {% endif %} |
| 233 | + {% if page_obj.has_next %} |
| 234 | + <a class="pagination-next" href="?page={{ page_obj.next_page_number }}">Next</a> |
| 235 | + {% else %} |
| 236 | + <a class="pagination-next" disabled>Next</a> |
| 237 | + {% endif %} |
| 238 | +</nav> |
| 239 | +{% endif %} |
| 240 | +``` |
| 241 | + |
| 242 | +After (with HTMX swap): |
| 243 | +```html |
| 244 | +<c-cf.pagination page="{{ page_obj.number }}" |
| 245 | + total_pages="{{ page_obj.paginator.num_pages }}" |
| 246 | + hx_url="{% url 'applications:main:ranked_job_list' %}" |
| 247 | + hx_target="#job-list" /> |
| 248 | +``` |
| 249 | + |
| 250 | +Without HTMX (full page reload fallback — cf-ui pagination uses hx-get, just omit hx_target): |
| 251 | +```html |
| 252 | +<c-cf.pagination page="{{ page_obj.number }}" |
| 253 | + total_pages="{{ page_obj.paginator.num_pages }}" |
| 254 | + hx_url="{% url 'applications:main:ranked_job_list' %}" |
| 255 | + hx_target="" /> |
| 256 | +``` |
| 257 | + |
| 258 | +--- |
| 259 | + |
| 260 | +### Navbar burger toggle |
| 261 | + |
| 262 | +The base layout uses vanilla JS to toggle the navbar burger. Once Alpine is loaded via `{% cf_ui_body %}`, replace it with `cfNavbar`. |
| 263 | + |
| 264 | +This is **optional** — the vanilla JS works fine. Migrate if you want to standardise on Alpine for all interactive patterns. |
| 265 | + |
| 266 | +Before (`snippets/nav.html`): |
| 267 | +```html |
| 268 | +<nav class="navbar is-link" role="navigation"> |
| 269 | + <div class="navbar-brand"> |
| 270 | + ... |
| 271 | + <a role="button" class="navbar-burger" aria-label="menu" |
| 272 | + data-target="mainNavbar"> |
| 273 | + <span aria-hidden="true"></span> |
| 274 | + <span aria-hidden="true"></span> |
| 275 | + <span aria-hidden="true"></span> |
| 276 | + </a> |
| 277 | + </div> |
| 278 | + <div class="navbar-menu" id="mainNavbar"> |
| 279 | + ... |
| 280 | + </div> |
| 281 | +</nav> |
| 282 | +``` |
| 283 | + |
| 284 | +After (Alpine-driven, remove `data-target` and the vanilla JS handler): |
| 285 | +```html |
| 286 | +<nav class="navbar is-link" role="navigation" x-data="cfNavbar"> |
| 287 | + <div class="navbar-brand"> |
| 288 | + ... |
| 289 | + <a role="button" class="navbar-burger" aria-label="menu" |
| 290 | + @click="toggle()" :class="{ 'is-active': menuOpen }"> |
| 291 | + <span aria-hidden="true"></span> |
| 292 | + <span aria-hidden="true"></span> |
| 293 | + <span aria-hidden="true"></span> |
| 294 | + </a> |
| 295 | + </div> |
| 296 | + <div class="navbar-menu" :class="{ 'is-active': menuOpen }"> |
| 297 | + ... |
| 298 | + </div> |
| 299 | +</nav> |
| 300 | +``` |
| 301 | + |
| 302 | +> Remove the `querySelectorAll(".navbar-burger")` block from the base layout `<script>` tag after this change. |
| 303 | +
|
| 304 | +--- |
| 305 | + |
| 306 | +## What NOT to migrate |
| 307 | + |
| 308 | +These are RankedJobs-specific components with custom BEM naming and design-system styling. Do not replace them with cf-ui equivalents — they are intentional and correct as-is: |
| 309 | + |
| 310 | +| Component | Reason | |
| 311 | +|---|---| |
| 312 | +| `job_card.html` | Custom `.rj-job-card` BEM component, not a generic Bulma card | |
| 313 | +| `tile.html` | Custom `.rj-tile` layout, image+text pattern | |
| 314 | +| `page_title.html` | Custom `.rj-page-title` heading with tokens | |
| 315 | +| `section_box.html` | Custom `.rj-section-box` container | |
| 316 | +| `snippets/nav.html` | Complex auth logic and dropdowns; migrate only the burger toggle (above) | |
| 317 | +| `snippets/footer.html` | Simple, fine as-is | |
| 318 | +| `crispy_form.html` | Crispy forms renders Bulma HTML directly; replacing with `CfFormField` would require switching away from crispy and rendering fields manually | |
| 319 | + |
| 320 | +--- |
| 321 | + |
| 322 | +## Suggested migration order |
| 323 | + |
| 324 | +1. **Settings + asset loading** — add `CfUiConfig` to INSTALLED_APPS, add `libraries` key, add `{% cf_ui_body %}` to base layout. Verify Alpine loads and the navbar burger still works (you haven't changed the markup yet). |
| 325 | + |
| 326 | +2. **Notifications** — migrate `snippets/message.html` and all inline `notification is-danger` divs. Low risk, high visibility. Run the full test suite after. |
| 327 | + |
| 328 | +3. **Modal** — replace the legacy Semantic UI `Job_Detail_Modal.html`. Already broken, so this is a net improvement regardless. |
| 329 | + |
| 330 | +4. **Progress bar** — migrate `gap_analysis.html`'s progress element. Consider driving `bar_type` from the Component class state rather than a Django template conditional. |
| 331 | + |
| 332 | +5. **Pagination** — migrate paginated list views to `<c-cf.pagination>`. Wire up `hx_target` if the list is an HTMX swap target. |
| 333 | + |
| 334 | +6. **Navbar burger** — optional last step. Replace the vanilla JS burger toggle with Alpine's `cfNavbar`. |
| 335 | + |
| 336 | +--- |
| 337 | + |
| 338 | +## Verification after each step |
| 339 | + |
| 340 | +```bash |
| 341 | +# Run the full Django test suite |
| 342 | +pytest --ds=settings.test -q |
| 343 | + |
| 344 | +# Visually check the migrated page in a browser |
| 345 | +python manage.py runserver |
| 346 | +``` |
| 347 | + |
| 348 | +For HTMX-driven components (`gap_analysis`, pagination), test the polling and swap behavior manually — unit tests won't cover the full HTMX cycle. |
0 commit comments